React Exit-Intent Panel

Genuinely useful for a real offer, genuinely hostile when overused. The restraint is built into the component rather than left to your discretion.

On a real page this fires once when the pointer leaves toward the browser chrome. Use the button to preview it here.

Installation

npx shadcn@latest add "https://kelvinui.com/registry/exit-intent.json"

Props

PropTypeDescription
titlestringHeadline
descriptionstringOne line on what's being offered
cta{ label, href }The offer
dismissLabelstringDecline text (default "No thanks")
armAfterMsnumberDelay before detection arms (default 5000)
forceOpenbooleanRender immediately, for testing
showTriggerbooleanManual trigger, used in these docs
onDismiss() => voidFires on any dismissal

Four constraints, all deliberate

  • Fires once. A ref latches on first trigger. Re-firing on every pointer excursion is what makes these universally hated.
  • Armed on a delay. Nothing binds for armAfterMs. Someone who lands and immediately leaves has not read anything; interrupting them is pure noise.
  • Top edge only. The handler ignores anything where clientY > 0 or relatedTarget is set, so leaving sideways, downward, or into an iframe does not count. Only movement toward the browser chrome is exit intent.
  • Pointer devices only. Guarded by matchMedia("(pointer: fine)"). There is no exit intent on touch — the equivalent implementations fire on scroll direction and interrupt people mid-read.

The decline is a plainly worded button of normal size, not a 9px guilt-tripped micro-link. If your offer needs a dark pattern to be accepted, the offer is the problem.

Make it worth the interruption: a real asset with no signup, as in the demo, not a discount on something they already declined. And if you also run a Sticky CTA Bar, pick one — a page with both is a page that fights its reader.

New components every week

Get the week's new Kelvin UI components and templates in one short email. No spam, unsubscribe anytime.