Design Lounge
Install

Nº 571 of 605

Honest exit-intent offer popup

A shop's leave-the-page popup with one offer and the real reason for it, an email field, a full-size No thanks, and a soft 640ms rise. No dark patterns.

Preview

Open full size
1280 × 800 · 15.0 KB · 200 lines

Use it

Free to use · credit appreciated

1 Once, in your project · Node 22.20+

npx skills add SusanAcharya/Design-Lounge

2 Then ask

Using the Design Lounge skill, build "Honest exit-intent offer popup" (piece popup-exit-offer) in my stack. Match its brief; restyle it onto my product's theme.

No skill? Paste the brief instead. It carries the numbers, tokens and behaviour.

Brief

200 lines

Build brief for a coding agent. Rebuild this piece in the reader’s stack. If they haven’t said which stack, ask once, then default to semantic HTML + CSS + a little vanilla JS. Match the numbers below; don’t “improve” them.

What it is

The popup a small online tea shop, Marlow & Pike, shows when a visitor moves to leave the loose-leaf page. It is a 760px split card: a 300px drawn still life on the left (a green Darjeeling tin, a steaming cup, a rotated “20% off loose-leaf” stamp) and the pitch on the right. The pitch gives one offer and the true reason for it: “We bought too much of this year’s Darjeeling. The autumn flush came in 40 kg over our order”. Then an email field, and two buttons of equal size: “No thanks” and “Send my code”. It rises 28px into place over 640ms. It must read as a shopkeeper being straight with you. No countdown, no “No, I hate saving money”, no hidden close button, no email wall in front of the code.

Reference behaviour

  1. Initial state: the popup is already open over the dimmed shop page (scrim rgba(42,31,24,.5)), focus in the email field. Below it, the shop’s product grid shows four tins.
  2. Exit intent: once per page view, when the pointer leaves the document through the top edge (mouseout with no relatedTarget and clientY <= 0), the popup opens. It never opens a second time by itself.
  3. “No thanks”, the 40px round close button, Escape and a scrim click all close it at once. A pill button, “See the Darjeeling offer”, appears bottom right so the visitor can come back, and takes focus.
  4. Send my code with an empty field: the field border turns oxblood and “Add an email so the code has somewhere to go.” appears below. With a malformed address: “That address looks incomplete. Check the part after the @.” Typing clears the message.
  5. A valid address swaps the right column for the result: “Sent to nell.ashworth@postbox.uk”, the headline “Here it is, in case the email is slow.”, the code FLUSH20 in a dashed 1.5px oxblood box with Copy code, the terms (“Works on everything in Loose leaf, including the 250 g caddies. Ends Sunday 1 November at midnight.”) and one primary, “Back to the teas”, which closes. The code is shown on screen, not held hostage in the inbox.
  6. Copy code copies where allowed and reads “Copied” for 1800ms.
  7. The steam over the cup is three strokes that drift up 26px and fade, 4.8s each, staggered 1.6s. It is the only loop.

Structure

1280 × 800
┌──────────────────────────────────────────────────────────────────────┐
│ Marlow & Pike  Loose leaf Teaware Gifts Visit          Basket (0)    │ 68
├──────────────────────────────────────────────────────────────────────┤
│ Loose-leaf tea (44px)                                               │
│ ┌──────┐  ┌──────────── dialog 760 × ~510 ──────────────┐ ┌──────┐  │
│ │ tin  │  │ ( 20% )        │ Before you go           [x] │ │ tin  │  │
│ │      │  │ stamp -8deg    │ We bought too much of       │ │      │  │
│ └──────┘  │                │ this year's Darjeeling.     │ └──────┘  │
│           │   art 300px    │ (34px Young Serif)          │           │
│           │   radial warm  │ reason paragraph, 40ch      │           │
│           │   ┌───┐  ~     │ Where should we send...     │           │
│           │   │tin│ (cup)  │ [you@example.com         ]  │           │
│           │   └───┘        │ [No thanks] [Send my code]  │           │
│           │ ~~~ ground ~~~ │ fine print 12.5px           │           │
│           └────────────────┴─────────────────────────────┘           │
└──────────────────────────────────────────────────────────────────────┘
  • <header class="nav"> and <main class="shop"> with a 4-column product grid (drawn mini tins on tinted tiles).
  • <dialog aria-labelledby="ot" aria-describedby="ow"> opened with showModal(); when open, display: grid; grid-template-columns: 300px 1fr.
    • .art (aria-hidden): a radial gradient sky radial-gradient(120% 80% at 20% 10%, #f6e3c4, #ecc994 45%, #d9a868), a 96px rotated stamp, and an inline SVG still life bottom-centred at 300px wide (tin and lid with left-lit linear gradients, label with Young Serif type, cup with a radial tea surface, three steam paths, two leaves, a soft ground shadow).
    • .body: close button absolutely top right; #ask (kicker, <h2>, reason, <form novalidate> with label, email input, alert line, a 2-column button grid, fine print); #done (hidden until success).
  • .peek pill, fixed bottom right, hidden while the popup is open.

Tokens

:root {
  /* paper and tea */
  --bg: #f1e9dc;          /* shop page */
  --card: #fbf6ee;        /* product cards */
  --panel: #fffaf2;       /* popup */
  --line: #e2d6c3;
  --edge: #8a7a66;        /* input and outline buttons, 4.00:1 on panel */
  --ink: #2a1f18;         /* 15.45:1 on panel */
  --ink-2: #5a4a3e;       /* 8.14:1 */
  --ink-3: #6f5f52;       /* 5.88:1, fine print, placeholder */
  --accent: #8b2f22;      /* oxblood: primary, stamp, kicker, errors; 8.00:1 as text */
  --accent-hover: #74261b;
  --accent-ink: #fff6ee;  /* 7.78:1 on accent */
  --accent-soft: #f3dfd6; /* hover fill, close hover */
  --tin: #2f4a3a;         /* illustration */
  --tin-2: #c99a3e;       /* lid */
  --scrim: rgba(42,31,24,.5);
  /* type */
  --serif: "Young Serif", Georgia, serif;
  --sans: "Lexend", system-ui, sans-serif;
  /* size */
  --w: 760px;
  --art: 300px;
  --r: 20px;
  --r-ctl: 12px;
  --h-ctl: 48px;
  --shadow: 0 2px 6px rgba(42,31,24,.08), 0 50px 100px -40px rgba(42,31,24,.6);
  /* motion */
  --t-micro: 160ms;
  --t-fade: 400ms;
  --t-rise: 640ms;
  --ease: cubic-bezier(.2,.7,.2,1);
  --ease-out: cubic-bezier(.16,1,.3,1);
  --ease-sheet: cubic-bezier(.32,.72,0,1);
}

Typography

RoleFamilySizeWeightLine-heightTrackingCase
Popup headlineYoung Serif34px (28px phone)4001.12-0.015emsentence, max 15ch
Shop h1Young Serif44px4001.1-0.015emsentence
WordmarkYoung Serif22px4001-0.01emtitle
Stamp figureYoung Serif30px40010numerals
CodeYoung Serif26px40010.06emUPPERCASE
KickerLexend13px5001.40sentence
Reason paragraphLexend15px300 (bold part 500)1.550sentence, max 40ch
Field labelLexend13px5001.40sentence
Input, buttonsLexend15px400 / 50010sentence
Fine printLexend12.5px3001.50sentence

Motion

ElementTriggerPropertyFrom → ToDurationEasingNotes
dialogopentranslateY28px → 0640ms--ease-outthe soft rise
dialogopenopacity0 → 1400ms--easefades faster than it rises
backdropopenbackgroundtransparent → scrim400ms--ease
steam pathsloopopacity, translateY0, 8px → .85 at 30% → 0, -18px4800ms--easestagger 1.6s
#donesuccessopacity, translateY0, 8px → 1, 0400ms--ease-out
buttonsactivetranslateY0 → 1px160ms--ease
sheet (< 640px)opentranslateY100% → 0640ms--ease-sheet

Reduced motion: no rise, no steam loop (steam paths sit still at .6 opacity), state swaps are instant.

States

  • Input: focus border --ink-2 with a 3px rgba(42,31,24,.1) ring (neutral, so focus never looks like an error); invalid border --accent, ring --accent-soft; message in --accent 13px/500 under the field.
  • No thanks: outline button, same 48px height and width as the primary. Hover --accent-soft fill.
  • Send my code: oxblood fill, hover --accent-hover.
  • Close: 40px circle, transparent, hover --accent-soft. On phones it sits on a rgba(255,250,242,.9) disc so it reads over the art.
  • Success: right column replaced; headline takes focus (tabindex="-1", no outline).
  • Copy code: label “Copied” for 1800ms.
  • Focus-visible: 2px --accent outline, 3px offset.

Accessibility

  • showModal() traps focus; Escape closes; the page behind is inert. Focus lands in the email field on open, on the success headline after submit, and on the “See the Darjeeling offer” pill after close.
  • The dialog is labelled by the headline and described by the reason paragraph, so a screen reader hears the offer and why.
  • The input has a visible <label> and autocomplete="email"; it is described by the error line (role="alert") and the fine print, so the email promise is read with the field.
  • The illustration is aria-hidden; the stamp repeats the offer that the paragraph already states.
  • Contrast: every text token passes 4.5:1 on the panel (figures in Tokens). The stamp text #fff6ee on #8b2f22 is 7.78:1. Field and outline-button borders are 4.00:1.
  • Targets: 48px buttons and field, 40px close, 44px reopen pill.
  • Dark-pattern rules, as acceptance items: the decline is a real button labelled “No thanks”, equal in size; the close button is visible; the popup never reopens itself; the code is shown on screen after submit.

Responsive rules

  • ≥ 1280: as specified, 760px.
  • 1024 and 768: the card keeps its two columns down to 792px viewport (760 + 2 × 16).
  • < 900: shop grid 2 columns, nav links hidden except Basket.
  • < 640: bottom sheet, full width, radius 22px 22px 0 0, scrolls inside. The art becomes a 150px band across the top with the SVG at 170px; the stamp shrinks to 76px. Headline 28px. The two buttons stay side by side at 50/50.

Acceptance checklist

Always

  • One offer, with a stated reason a customer could check, and a stated end date.
  • The decline button is labelled plainly, has the same size as the primary, and closes at once.
  • A visible close button, Escape and a scrim click all close.
  • Exit intent fires at most once per page view and never after a dismissal; a pill lets the visitor reopen.
  • Validation messages say what to fix, appear under the field, and clear on input.
  • After submit the code is shown on screen with Copy, not only promised by email.
  • The fine print says how many emails will follow.
  • The rise is 28px over 640ms with opacity on a shorter 400ms clock; reduced motion removes it and stops the steam.
  • Below 640px it is a full-width bottom sheet with no sideways scroll at 390px.

This demo

  • Headline: “We bought too much of this year’s Darjeeling.” in Young Serif 34px.
  • Offer: 20% off any loose-leaf tea until Sunday 1 November; code FLUSH20.
  • Art: green tin with a “Darjeeling / AUTUMN FLUSH” label, cup with three steam strokes, oxblood stamp rotated -8deg.

Implementation notes

Two clocks for a soft rise. Fade and rise on separate durations so the card is visible early and settles late. @starting-style gives the entry without JS.

dialog { opacity: 0; transform: translateY(28px);
  transition: opacity 400ms var(--ease), transform 640ms var(--ease-out),
              overlay 640ms allow-discrete, display 640ms allow-discrete; }
dialog[open] { opacity: 1; transform: none; display: grid; grid-template-columns: 300px 1fr; }
@starting-style { dialog[open] { opacity: 0; transform: translateY(28px); } }

Exit intent that respects a no. Fire only when the pointer leaves through the top and only if nothing has shown yet. Track it in memory for this page, or in your own consent-aware storage for the session.

let shown = false;
document.addEventListener('mouseout', (e) => {
  if (!e.relatedTarget && e.clientY <= 0 && !shown) open();
});
function open() { if (dlg.open) return; shown = true; dlg.showModal(); email.focus(); }

Left-lit cylinders in SVG. One horizontal gradient per object, light on the left third, dark on the right, sells the volume without filters.

<linearGradient id="tin" x1="0" x2="1">
  <stop offset="0" stop-color="#3f604c"/><stop offset=".35" stop-color="#2f4a3a"/><stop offset="1" stop-color="#1c2e24"/>
</linearGradient>

Common mistakes: a shaming decline link in 11px grey; a countdown timer that resets on reload; opening on page load for first-time visitors on a phone (there is no exit intent on touch, so use a scroll-depth or never); stacking the offer behind an email wall; a stock photo instead of a drawn object in the shop’s own colours.

Details

Palette
Type
Young Serif · Lexend
Motion
Subtle motion
Build
One session
Designed bySusan AcharyaKathmandu

Studied

Don't Board Me

A control. It studies this site for quiet, not for a show.

All sources