Preview
Source
tsx
"use client";
import { useState, useEffect, useCallback } from "react";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { ArrowRight, Gift } from "lucide-react";
const SESSION_KEY = "innovations_exit_intent_shown";
export default function ExitIntent() {
const [open, setOpen] = useState(false);
const [email, setEmail] = useState("");
const [submitted, setSubmitted] = useState(false);
const [firedOnce, setFiredOnce] = useState(false);
const fire = useCallback(() => {
if (firedOnce) return;
setFiredOnce(true);
setOpen(true);
sessionStorage.setItem(SESSION_KEY, "true");
}, [firedOnce]);
// SSR-guarded exit intent listener
useEffect(() => {
if (sessionStorage.getItem(SESSION_KEY)) return;
function onMouseLeave(e: MouseEvent) {
if (e.clientY <= 0) {
fire();
}
}
document.addEventListener("mouseleave", onMouseLeave);
return () => document.removeEventListener("mouseleave", onMouseLeave);
}, [fire]);
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (!email) return;
setSubmitted(true);
}
return (
<div className="flex min-h-[200px] flex-col items-center justify-center gap-4 p-8">
{/* Demo trigger */}
<Button
variant="outline"
onClick={fire}
>
Trigger exit intent (demo)
</Button>
<p className="text-xs text-muted-foreground text-center max-w-xs">
In production, this fires automatically when the cursor leaves the top of the viewport.
Uses sessionStorage so it shows at most once per session.
</p>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="sm:max-w-md overflow-hidden p-0">
{/* Gradient header */}
<div className="bg-gradient-to-br from-violet-600 to-purple-700 p-8 text-white text-center">
<div className="mx-auto mb-3 flex h-14 w-14 items-center justify-center rounded-full bg-white/20 backdrop-blur-sm">
<Gift className="h-7 w-7 text-white" />
</div>
<p className="text-xs font-semibold uppercase tracking-widest text-white/70 mb-1">
Wait — before you go
</p>
<h2 className="text-2xl font-extrabold leading-tight">
Grab this free before you leave
</h2>
</div>
<div className="p-6 sm:p-8">
{submitted ? (
<div className="py-4 text-center space-y-3">
<div className="text-4xl">🎉</div>
<h3 className="font-bold text-lg text-foreground">It's in your inbox!</h3>
<p className="text-sm text-muted-foreground">
We sent the guide to <strong>{email}</strong>. Check your spam folder if you
don't see it within 5 minutes.
</p>
<Button onClick={() => setOpen(false)} className="mt-2">
Close
</Button>
</div>
) : (
<>
<DialogHeader className="text-left mb-5">
<DialogTitle className="text-lg font-bold">The Growth Playbook</DialogTitle>
<DialogDescription className="text-base text-muted-foreground">
The exact framework we use to help clients 3x revenue in under 90 days.
Enter your email and we'll send it instantly.
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-3">
<Input
type="email"
placeholder="[email protected]"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
className="h-11"
/>
<Button type="submit" className="w-full h-11 gap-2 font-semibold">
Send me the free guide
<ArrowRight className="h-4 w-4" />
</Button>
</form>
<button
onClick={() => setOpen(false)}
className="mt-4 block w-full text-center text-xs text-muted-foreground hover:text-foreground transition-colors"
>
No thanks, I'll pass on the free guide
</button>
</>
)}
</div>
</DialogContent>
</Dialog>
</div>
);
} Claude Code Instructions
CLI Install
npx innovations add exit-intentWhere to use it
Add to your root layout so exit intent detection is active on every page.
In Next.js root layout (app/layout.tsx):
import ExitIntent from '@/components/innovations/freebies/exit-intent';
In Astro root layout:
<ExitIntent client:load />
The mouseleave listener and sessionStorage check are inside useEffect — fully SSR-safe.
The popup fires at most once per browser session (sessionStorage key).
Remove the demo trigger button before shipping to production.
Wire the form to your email provider. Consider A/B testing different offers.