A Guide to Build an Exit Intent Popup React

A Guide to Build an Exit Intent Popup React

exit intent popup react
react popup component
react exit intent
cart abandonment
react hooks
Share this post:

You're probably here because the basic React examples aren't enough anymore. The desktop version works, you can fire a modal when the mouse leaves the viewport, and then the first real problem shows up. Mobile traffic doesn't have a cursor, your popup flashes at the wrong time, or the modal technically opens but feels rough, inaccessible, and impossible to maintain.

That's where most exit intent popup React guides stop. Production work starts there.

A solid implementation does three jobs at once. It detects likely abandonment without being trigger-happy, it renders a modal that keyboard and screen reader users can use, and it behaves differently on desktop and mobile because those are different interaction models, not just different screen widths.

Why You Need an Exit Intent Popup

A shopper can browse multiple product pages, add something to cart, hesitate, and then leave in a single motion. On desktop, that moment is often visible. The cursor drifts toward the browser controls, and you get one short window to offer something useful before the session disappears.

That's why exit intent is still practical. It doesn't interrupt the visit early. It waits until the user shows a leaving signal, then gives you one last chance to recover the session.

Wisepops reports that exit popups convert at an average rate of 2.81% of visitors, while top campaigns reach 19.63%, which makes the upside clear but also shows how much depends on the offer, timing, and targeting, not the popup alone (Wisepops benchmark data).

What makes exit intent worth building

The value isn't just “show a discount before someone leaves.” That approach gets old fast. Its core value is precision.

  • Late-session timing: You intervene at the point of churn rather than blocking product discovery.
  • Flexible outcomes: You can recover a cart, collect an email, offer support, or ask a quick reason-for-leaving question.
  • Low surface area: One reusable trigger can support many flows across product, cart, and content pages.

A lot of teams also connect the popup to follow-up channels. If your exit flow captures an email, it helps to have the next message ready instead of treating collection as the finish line. That's where resources like Pipeline On's sales email templates are useful, especially when you need practical copy patterns for follow-up and recovery.

What doesn't work

Bad exit intent implementations fail for predictable reasons:

ProblemWhat happens
Popup fires too earlyUsers feel interrupted, not helped
Offer is genericShoppers ignore it because it doesn't match intent
Repeat display isn't controlledReturning users get the same modal over and over
Mobile uses desktop logicNormal touch behavior gets mistaken for abandonment

Practical rule: Exit intent should feel like a last helpful intervention, not a trap.

If you want to see how the tactic is commonly used in ecommerce, Cart Whisper's overview of exit-intent popups is a useful reference point. The implementation details matter, but the business case is simple. You already paid to earn the session. Exit intent gives you one final shot at saving it.

Detecting Exit Intent with a Custom React Hook

A lot of examples wire event listeners directly into page components. That works once, then turns into duplication. Detection logic belongs in a hook because the behavior is cross-cutting, stateful, and easy to reuse.

For desktop, the core pattern is still event-driven. Modern React implementations typically watch mouse movement or mouseout behavior and flip component state when the pointer moves toward browser chrome. That's the basic foundation behind most exit intent popup React setups.

A flowchart infographic explaining the five steps to create a custom exit intent hook in React.
A flowchart infographic explaining the five steps to create a custom exit intent hook in React.

A clean desktop hook

import { useEffect, useRef, useState } from "react"; export function useExitIntent({ enabled = true, threshold = 12, once = true, shouldSuppress, } = {}) { const [hasTriggered, setHasTriggered] = useState(false); const frame = useRef(null); useEffect(() => { if (!enabled) return; if (once && hasTriggered) return; if (shouldSuppress?.()) return; const onMouseOut = (event) => { if (frame.current) return; frame.current = requestAnimationFrame(() => { frame.current = null; const toElement = event.relatedTarget || event.toElement; const leavingWindow = !toElement; const nearTopEdge = event.clientY <= threshold; if (leavingWindow && nearTopEdge) { setHasTriggered(true); } }); }; document.addEventListener("mouseout", onMouseOut); return () => { document.removeEventListener("mouseout", onMouseOut); if (frame.current) cancelAnimationFrame(frame.current); }; }, [enabled, threshold, once, hasTriggered, shouldSuppress]); return { showExitIntent: hasTriggered, resetExitIntent: () => setHasTriggered(false), }; }

This does a few important things without getting fancy. It keeps the listener centralized, prevents noisy event handling by coalescing work with requestAnimationFrame, and cleans up properly on unmount.

Why this shape holds up in production

You don't want every page deciding for itself how exit intent works. You want a common contract.

A good hook should let you:

  • Enable or disable detection by route: Product pages and cart pages often deserve different behavior.
  • Suppress for converted users: If checkout is complete, stop listening.
  • Reset when needed: Some flows need controlled reopening during testing or multi-step UX.

If your app architecture is still in flux between SPA patterns and server-rendered routes, Four Eyes on React vs Next.js is a solid comparison for deciding where client-side behavior like this should live.

Don't skip frequency control

One of the easiest ways to ruin an exit popup is to show it every visit. A common implementation detail is cookie-based suppression. One tutorial example uses a modal_seen cookie that expires after 14 days, which is a practical way to avoid hammering returning visitors with the same prompt (Dazze on exit-intent suppression).

function shouldSuppressExitIntent() { return document.cookie.includes("modal_seen=true"); } function markExitIntentSeen() { const expires = new Date(); expires.setDate(expires.getDate() + 14); document.cookie = `modal_seen=true; expires=${expires.toUTCString()}; path=/`; }

Good hook design isn't about detecting every possible leave signal. It's about detecting the right one once, then getting out of the user's way.

Building a Reusable and Accessible Popup Component

Detection is only half the job. If the modal is sloppy, users feel it immediately. The biggest misses are always the same. Focus escapes behind the overlay, Escape doesn't close, the close button has no label, and the dialog structure makes no sense to assistive tech.

A reusable popup should solve those problems once.

The minimum accessible contract

Your modal should expose the semantics screen readers expect:

  • role="dialog"
  • aria-modal="true"
  • aria-labelledby pointing to the visible title
  • aria-describedby when supporting copy needs to be announced

The modal also needs a real close button, not just a clickable icon. Give it an accessible label.

import { useEffect, useRef } from "react"; export function ExitIntentModal({ open, titleId = "exit-intent-title", descriptionId = "exit-intent-description", onClose, children, }) { const dialogRef = useRef(null); const previousFocusRef = useRef(null); useEffect(() => { if (!open) return; previousFocusRef.current = document.activeElement; const dialog = dialogRef.current; const focusable = dialog.querySelectorAll( 'a[href], button:not([disabled]), textarea, input, select, [tabindex]:not([tabindex="-1"])' ); const first = focusable[0]; const last = focusable[focusable.length - 1]; first?.focus(); const onKeyDown = (event) => { if (event.key === "Escape") { onClose(); return; } if (event.key === "Tab" && focusable.length > 0) { if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last?.focus(); } else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first?.focus(); } } }; document.addEventListener("keydown", onKeyDown); return () => { document.removeEventListener("keydown", onKeyDown); previousFocusRef.current?.focus?.(); }; }, [open, onClose]); if (!open) return null; return ( <div className="overlay" onClick={onClose}> <div ref={dialogRef} role="dialog" aria-modal="true" aria-labelledby={titleId} aria-describedby={descriptionId} className="modal" onClick={(e) => e.stopPropagation()} > <button aria-label="Close popup" onClick={onClose} className="close"> × </button> {children} </div> </div> ); }

Focus management matters more than styling

Teams often spend more time on blur effects than keyboard flow. That's backwards. If focus doesn't move into the dialog when it opens, keyboard users are still interacting with the page behind it. If focus isn't restored on close, the user loses context.

That's why I treat focus management as essential behavior, not polish.

A few practical details make the component sturdier:

ConcernBetter choice
Overlay clickClose only when the user clicks outside the dialog
Escape keyAlways supported
Return focusSend users back to the previously active element
Form fieldsAutofocus the first logical field, not always the close button

Keep the API flexible

Don't hard-code discount copy into the component. The modal should be generic enough to support:

  • Cart recovery offers
  • Newsletter capture
  • Live support prompts
  • Feedback collection
  • “Need help before you go?” service flows

Accessibility check: If someone can't open, use, and dismiss your popup with only a keyboard, the component isn't production-ready.

Animations are fine, but keep them subtle. Fade and scale transitions usually work better than dramatic movement. The popup should feel responsive, not theatrical.

Solving Exit Intent on Mobile Devices

Most exit intent popup React tutorials implicitly assume a mouse exists. That's the core mistake. On mobile, mouseout doesn't help because touch interfaces don't produce the same leaving signal. Treating mobile as “desktop but smaller” creates false positives and annoying popups.

Current guidance points to a gap here. Most React examples focus on desktop cursor movement, while mobile exit intent is a separate problem that usually depends on quick upward scrolling or back-button behavior (Fullstack Heroes on React exit intent).

A comparison infographic showing that desktop exit intent is cursor-driven while mobile exit intent is behavior-driven.
A comparison infographic showing that desktop exit intent is cursor-driven while mobile exit intent is behavior-driven.

What mobile signals can actually tell you

None of the common mobile signals are perfect on their own.

  • Rapid upward scroll: Sometimes indicates the user is moving toward browser UI or reconsidering the session. It also happens during ordinary reading.
  • Back-button interaction: Stronger intent signal, but hard to use cleanly without affecting navigation behavior.
  • Tab switch or page visibility changes: Useful in some cases, but noisy and dependent on browser behavior.

The fix isn't to find a magical mobile event. The fix is combining weak signals carefully and adding stricter gating.

A practical mobile fallback

The most reliable mobile approach I've used is rule-based. Don't fire on the first upward movement. Wait until the user has shown engagement, then detect a sharp reversal that looks like abandonment rather than casual scrolling.

import { useEffect, useRef, useState } from "react"; export function useMobileExitIntent({ enabled = true, minScrollY = 300 } = {}) { const [triggered, setTriggered] = useState(false); const lastY = useRef(0); const lastTime = useRef(Date.now()); useEffect(() => { if (!enabled || triggered) return; const onScroll = () => { const currentY = window.scrollY; const currentTime = Date.now(); const deltaY = currentY - lastY.current; const deltaTime = currentTime - lastTime.current || 1; const scrollingUpFast = deltaY < 0 && Math.abs(deltaY / deltaTime) > 1; const userWasEngaged = currentY > minScrollY || lastY.current > minScrollY; if (userWasEngaged && scrollingUpFast) { setTriggered(true); } lastY.current = currentY; lastTime.current = currentTime; }; window.addEventListener("scroll", onScroll, { passive: true }); return () => window.removeEventListener("scroll", onScroll); }, [enabled, triggered, minScrollY]); return triggered; }

This is still heuristic. That's fine. Mobile exit intent is heuristic.

Separate desktop and mobile rules

The most important architectural decision is not the exact threshold. It's splitting device logic so each mode has its own trigger path.

const isTouchDevice = typeof window !== "undefined" && window.matchMedia("(pointer: coarse)").matches;

Use desktop pointer logic for fine pointers. Use scroll or navigation heuristics for coarse pointers. Don't merge them into one event soup.

Mobile exit intent should be more conservative than desktop. If you're unsure, suppress the popup.

That trade-off is worth it. Missing a few edge cases is better than turning routine mobile scrolling into a constant interruption.

Best Practices for High-Converting Popups

A working popup can still perform badly. That usually has nothing to do with React and everything to do with judgment. You can ship clean code and still hurt conversions with weak offers, bad segmentation, or blunt timing.

Kissmetrics notes that well-implemented exit-intent overlays can recover about 10–15% of abandoning visitors, but that depends heavily on segmentation, offer relevance, and strict suppression rules (Kissmetrics on exit-intent strategies).

An infographic detailing six essential strategies for designing and implementing high-converting website popups for improved user engagement.
An infographic detailing six essential strategies for designing and implementing high-converting website popups for improved user engagement.

The offer decides whether the popup deserves attention

Users don't care that your detection logic is elegant. They care whether the message is useful.

A few examples of stronger fit:

  • Cart page: Shipping reassurance, discount reminder, or support prompt
  • Product page: Size help, comparison help, or first-order incentive
  • Content page: Resource download or email capture tied to the article topic

Weak fit is easy to spot. Generic “Wait! Don't leave!” copy with no context almost always feels disposable.

Suppression rules are part of conversion strategy

The best-performing popups are usually the least repetitive. Good rules include:

RuleWhy it matters
Exclude already converted usersAvoid pointless interruption
Show once per sessionPrevent fatigue
Suppress after form completionRespect the action already taken
Gate by page intentCart and product pages often deserve different copy

If you need examples of message angles and offer formats, these exit-intent popup examples are useful for comparing how different prompts align with different stages of buying intent.

Track behavior, not just submissions

The popup submit rate is only one slice of the picture. I prefer tracking a small event set:

  • Popup viewed
  • Dismissed
  • Primary CTA clicked
  • Form submitted
  • Recovered checkout or resumed cart

That event chain tells you whether the issue is detection, offer quality, or UI friction.

Cart recovery tools can also help when you need visibility into behavior around abandonment. For example, Cart Whisper | Live View Pro includes exit-intent popups alongside live cart activity and shopper behavior data, which is useful when teams want both intervention and session context in one workflow.

If the popup gets views but few clicks, rewrite the offer. If it gets clicks but poor completion, fix the form or follow-through. Don't keep tuning trigger logic for a copy problem.

Putting It All Together for Smarter Cart Recovery

A production-ready exit intent popup React implementation isn't one component. It's a small system. Detection logic lives in a reusable hook, the UI lives in an accessible modal, and the trigger rules differ by device because desktop and mobile abandonment don't look the same.

That combination changes the quality of the result. Desktop detection can rely on pointer behavior near browser chrome. Mobile needs behavior-based heuristics and stricter suppression. The popup itself has to respect keyboard flow, announce itself correctly, and close cleanly without leaving focus in limbo.

The strategic layer matters just as much. Even a polished implementation won't rescue many sessions if the offer is irrelevant, repeated too often, or shown to the wrong audience. The strongest setups tie trigger rules to page intent, suppress repeat displays aggressively, and instrument the full path from popup view to recovered checkout.

Exit intent also has a limit. It tells you that a shopper is leaving. It doesn't automatically tell you why. That's where broader cart visibility becomes useful. If you can see what the shopper viewed, changed, searched, or removed before abandoning, you can fix friction earlier instead of only reacting at the last second. For stores working on the bigger abandonment problem, this guide on how to reduce shopping cart abandonment is a good next step.


If you want more than a standalone popup, Cart Whisper | Live View Pro gives Shopify merchants real-time cart and shopper visibility, plus exit-intent popups and targeted recovery widgets. That combination helps teams spot abandonment signals earlier, respond faster, and connect recovery actions to actual cart behavior instead of guessing.