Exit Intent Popup
atroui/exit-intent-popupFreeBlock
Exit-intent dialog with editable CONTENT and storage keys.
Install it
npx shadcn@latest add https://atroui.com/r/exit-intent-popup.jsonSource
1"use client"23import { X } from "lucide-react"4import Link from "next/link"5import { useCallback, useEffect, useRef, useState } from "react"67/**8 * Edit CONTENT to change exit-intent copy and storage keys.9 */10const CONTENT = {11 stamp: "Before you go",12 title: "Free 7-day MVP scoping",13 body: "Not sure what to build first? Book a no-pressure scoping call - we'll map scope, timeline, and budget in one session.",14 primaryLabel: "Start free scoping",15 primaryHref: "/scope",16 secondaryLabel: "Maybe later",17 storageKey: "atroui_exit_intent_v1",18 sessionKey: "atroui_exit_intent_session",19}2021function shouldShowPopup(): boolean {22 if (typeof window === "undefined") return false23 if (localStorage.getItem(CONTENT.storageKey)) return false24 if (sessionStorage.getItem(CONTENT.sessionKey)) return false25 return true26}2728function markShown() {29 sessionStorage.setItem(CONTENT.sessionKey, "1")30}3132function markDismissed() {33 localStorage.setItem(CONTENT.storageKey, "dismissed")34 sessionStorage.setItem(CONTENT.sessionKey, "1")35}3637export type ExitIntentPopupProps = {38 /** Docs / demos: open immediately and render inline. */39 preview?: boolean40}4142export function ExitIntentPopup({ preview = false }: ExitIntentPopupProps) {43 const [open, setOpen] = useState(preview)44 const dialogRef = useRef<HTMLDivElement>(null)45 const triggered = useRef(false)4647 const close = useCallback(() => {48 setOpen(false)49 if (!preview) markDismissed()50 }, [preview])5152 useEffect(() => {53 if (preview) return54 if (!shouldShowPopup()) return5556 const onMouseLeave = (e: MouseEvent) => {57 if (triggered.current) return58 if (e.clientY > 12) return59 if (!shouldShowPopup()) return60 triggered.current = true61 markShown()62 setOpen(true)63 }6465 document.documentElement.addEventListener("mouseleave", onMouseLeave)66 return () =>67 document.documentElement.removeEventListener("mouseleave", onMouseLeave)68 }, [preview])6970 useEffect(() => {71 if (!open || preview) return7273 const onKeyDown = (e: KeyboardEvent) => {74 if (e.key === "Escape") close()75 }7677 document.addEventListener("keydown", onKeyDown)78 dialogRef.current?.focus()7980 const focusable = dialogRef.current?.querySelectorAll<HTMLElement>(81 'a[href], button:not([disabled]), [tabindex]:not([tabindex="-1"])',82 )83 const first = focusable?.[0]84 const last = focusable?.[focusable.length - 1]8586 const trapFocus = (e: KeyboardEvent) => {87// … truncated
What it pulls in
npm packages
Files it writes
More from atroui
All 82 items| Component | Registry | Kind | Access | Installs | Command |
|---|---|---|---|---|---|
| Analytics Provideranalytics-provider | atroui | Blocks | Free | 1 dep | |
| AR Portfolioar-portfolio | atroui | Blocks | Free | 1 dep | |
| Before / Afterbefore-after-slider | atroui | Blocks | Free | no deps | |
| Calendly Embedcalendly-embed | atroui | Blocks | Free | 1 dep | |
| Changelogchangelog | atroui | Blocks | Free | no deps | |
| Command Menucommand-menu | atroui | Blocks | Free | 4 deps | |
| Contact Formcontact-form | atroui | Blocks | Free | 2 deps | |
| Contextual CTAcontextual-cta | atroui | Blocks | Free | 2 deps |
