Detects when users are about to leave the page. On desktop, it tracks mouse movement toward the browser chrome. On mobile, it uses the visibility change API.
Installation
npm install betteruseUsage
import { useExitIntent } from 'betteruse'
function ExitPopup() {
const [showModal, setShowModal] = useState(false)
const { hasExited, reset } = useExitIntent({
onExitIntent: () => setShowModal(true),
threshold: 20,
triggerOnce: true,
})
return (
<>
{showModal && (
<Modal onClose={() => {
setShowModal(false)
reset()
}}>
<h2>Wait! Don't go yet!</h2>
<p>Subscribe to our newsletter for updates.</p>
</Modal>
)}
</>
)
}
API Reference
useExitIntent(options)
| Prop | Type | Default | Description |
|---|---|---|---|
onExitIntent* | () => void | - | Callback when exit intent is detected |
threshold | number | 20 | Threshold in pixels from top of viewport |
triggerOnce | boolean | true | Only trigger once |
delayMs | number | 0 | Delay before triggering in milliseconds |
includeVisibilityChange | boolean | true | Include visibility change events for mobile |
enabled | boolean | true | Whether the hook is enabled |
Return Value
| Prop | Type | Default | Description |
|---|---|---|---|
hasExited | boolean | - | Whether exit intent has been triggered |
reset | () => void | - | Reset the exit state to allow re-triggering |
How It Works
Desktop Detection
On desktop, the hook listens for the mouseleave event on the document. When the mouse leaves the viewport near the top (within the threshold), it triggers the exit intent.
Mobile Detection
On mobile devices, users don't have a visible cursor. Instead, the hook uses the Page Visibility API to detect when:
- The user switches tabs
- The user minimizes the browser
- The user switches apps
This hook is SSR-safe. Event listeners are only added on the client.
Examples
Newsletter Popup
function NewsletterPopup() {
const [shown, setShown] = useState(false)
const [dismissed, setDismissed] = useState(false)
useExitIntent({
onExitIntent: () => {
if (!dismissed) {
setShown(true)
}
},
triggerOnce: true,
})
if (!shown) return null
return (
<div className="fixed inset-0 flex items-center justify-center bg-black/50">
<div className="bg-white p-8 rounded-lg">
<h2>Before you go...</h2>
<p>Sign up for our newsletter!</p>
<input type="email" placeholder="your@email.com" />
<button onClick={() => setShown(false)}>Subscribe</button>
<button onClick={() => {
setDismissed(true)
setShown(false)
}}>
No thanks
</button>
</div>
</div>
)
}
Cart Abandonment
function CartPage() {
const { hasExited } = useExitIntent({
onExitIntent: () => {
// Track abandonment
analytics.track('cart_abandonment_intent')
},
triggerOnce: true,
enabled: cartItems.length > 0,
})
return <div>...</div>
}
With Delay
function DelayedPopup() {
const [show, setShow] = useState(false)
useExitIntent({
onExitIntent: () => setShow(true),
delayMs: 500, // Wait 500ms before showing
triggerOnce: true,
})
return show ? <Popup /> : null
}
Best Practices
- Don't be annoying: Use
triggerOnce: trueto avoid showing the popup multiple times - Provide value: Make sure your popup offers something valuable to the user
- Easy dismissal: Always provide a clear way to close the popup
- Respect preferences: Consider storing whether the user dismissed the popup