Nº 149 of 520
Sticky mobile CTA bar
Sprout product page on a 390×844 phone: a sticky bottom bar keeps $48 and Add to bag, and hides the description once the page is scrolled 72px.
Preview
Use it
Free to use · credit appreciated1 Once, in your project
npx skills add SusanAcharya/Design-Lounge 2 Then ask
Using the Design Lounge skill, build "Sticky mobile CTA bar" (piece cta-sticky-mobile-bar) 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.
<!-- Design Lounge Nº 149 · "Sticky mobile CTA bar" · www.designlounge.live -->
# Sticky mobile CTA bar
> **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
A short product page for Sprout, a fictional houseplant starter kit, framed at **390×844** (`mobile-web`). The Lounge draws the phone chrome, so the page leaves **54px** at the top and **34px** at the bottom clear of fixed UI. A sticky bottom bar sits on that 34px inset: expanded it shows a one-line description, the price, and "Add to bag"; after the page scrolls **72px** the description collapses to `max-height: 0` and the bar shortens. Price and button never leave. One family, Sora. Coral `#d4533e` is the only saturated colour; leaf green is reserved for the kicker and icons.
## Reference behaviour
1. Initial state: cool off-white `#f4f5f2`. Top inset 54px, then a 52px sticky nav ("Sprout" + Kit). Hero: 11px uppercase kicker "Houseplant kit · ships Thursday", 32px/600 title "Starter kit for a first windowsill", 15px lede, a 168px CSS seedling illustration, `$48` with "one kit · free post over $40".
2. Sticky CTA bar is `position: fixed; left: 0; right: 0; bottom: 34px` (or `env(safe-area-inset-bottom, 34px)`). Surface white, 1px top hairline, padding 12/20, grid `1fr auto` with the description spanning both columns on row 1. Description: "Starter kit · 12 weeks of seed. Ships Thursday." Price `$48` + "incl. post". Button 48px tall, pill, coral, 14px/600 "Add to bag", min-width 128px.
3. On `scroll`, if `scrollY > 72`, add `.compact` to the bar: padding 10/20, description `max-height: 0; opacity: 0`, hide the "incl. post" small. Price and button stay on one row. If `scrollY ≤ 72`, remove `.compact`. Nav gets `.stuck` (1px bottom border) when `scrollY > 8`.
4. Body padding-bottom is `34px + 92px + 8px` so the last review is not hidden behind the expanded bar.
5. Add to bag writes "Starter kit added · $48" into a toast (`role="status"`) above the bar, shown 2200ms. Toast does not persist.
6. Reduced motion: `scroll-behavior: auto`; bar, description, toast and button transitions become 1ms. Compact still toggles, no lag.
7. Hit targets: nav links 40px-class padding, button 48px, bar remains tappable in compact (button stays 48px).
## Structure
```
390 × 844
┌─────────────────────────────┐
│ (54px lounge status) │
│ Sprout Kit │ sticky 52, top: 54
│ HOUSEPLANT KIT · SHIPS… │
│ Starter kit for a first │ h1 32/600
│ windowsill │
│ Soil, a 12-week seed tin… │
│ ┌─────────────────────────┐ │ mock 168, CSS plant
│ └─────────────────────────┘ │
│ $48 one kit · free post │
│ IN THE BOX │
│ • 2.4 L living soil │
│ • Seed tin, 12 weeks │
│ • Watering plan │
│ FROM A WINDOWSILL IN PORTO │
│ two review cards │
… 152 more lines. Copy or download for the full brief. Loading the source…Brief
192 linesBuild 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
A short product page for Sprout, a fictional houseplant starter kit, framed at 390×844 (mobile-web). The Lounge draws the phone chrome, so the page leaves 54px at the top and 34px at the bottom clear of fixed UI. A sticky bottom bar sits on that 34px inset: expanded it shows a one-line description, the price, and “Add to bag”; after the page scrolls 72px the description collapses to max-height: 0 and the bar shortens. Price and button never leave. One family, Sora. Coral #d4533e is the only saturated colour; leaf green is reserved for the kicker and icons.
Reference behaviour
- Initial state: cool off-white
#f4f5f2. Top inset 54px, then a 52px sticky nav (“Sprout” + Kit). Hero: 11px uppercase kicker “Houseplant kit · ships Thursday”, 32px/600 title “Starter kit for a first windowsill”, 15px lede, a 168px CSS seedling illustration,$48with “one kit · free post over $40”. - Sticky CTA bar is
position: fixed; left: 0; right: 0; bottom: 34px(orenv(safe-area-inset-bottom, 34px)). Surface white, 1px top hairline, padding 12/20, grid1fr autowith the description spanning both columns on row 1. Description: “Starter kit · 12 weeks of seed. Ships Thursday.” Price$48+ “incl. post”. Button 48px tall, pill, coral, 14px/600 “Add to bag”, min-width 128px. - On
scroll, ifscrollY > 72, add.compactto the bar: padding 10/20, descriptionmax-height: 0; opacity: 0, hide the “incl. post” small. Price and button stay on one row. IfscrollY ≤ 72, remove.compact. Nav gets.stuck(1px bottom border) whenscrollY > 8. - Body padding-bottom is
34px + 92px + 8pxso the last review is not hidden behind the expanded bar. - Add to bag writes “Starter kit added · $48” into a toast (
role="status") above the bar, shown 2200ms. Toast does not persist. - Reduced motion:
scroll-behavior: auto; bar, description, toast and button transitions become 1ms. Compact still toggles, no lag. - Hit targets: nav links 40px-class padding, button 48px, bar remains tappable in compact (button stays 48px).
Structure
390 × 844
┌─────────────────────────────┐
│ (54px lounge status) │
│ Sprout Kit │ sticky 52, top: 54
│ HOUSEPLANT KIT · SHIPS… │
│ Starter kit for a first │ h1 32/600
│ windowsill │
│ Soil, a 12-week seed tin… │
│ ┌─────────────────────────┐ │ mock 168, CSS plant
│ └─────────────────────────┘ │
│ $48 one kit · free post │
│ IN THE BOX │
│ • 2.4 L living soil │
│ • Seed tin, 12 weeks │
│ • Watering plan │
│ FROM A WINDOWSILL IN PORTO │
│ two review cards │
│─────────────────────────────│
│ Starter kit · 12 weeks of seed. Ships Thursday. │ desc, hides when compact
│ $48 [Add to bag]│ always visible
│ (34px lounge home) │ bar bottom: 34
└─────────────────────────────┘
<header class="nav" id="nav">sticky,top: var(--safe-top)..hero#top: kicker, h1, lede,.mock(decorativerole="img"),.price-hero.<section id="kit">: “In the box” + three<li>with 20px stroke icons.- Reviews section: two
.revcards. <div class="bar" id="bar">:<p class="desc">,.amt,<button class="add" id="add">.<div class="toast" id="toast" role="status" aria-live="polite">.
Viewport meta: width=device-width, initial-scale=1. First HTML comment: platform: mobile-web · 390x844.
Tokens
:root {
--bg: #f4f5f2; /* page */
--surface: #fff; /* bar, reviews */
--sunk: #e8ebe4;
--ink: #171c18;
--ink-2: #5a635c;
--ink-3: #8b928c;
--line: #d8ddd4;
--accent: #d4533e; /* Add to bag */
--accent-ink: #fff;
--leaf: #3f6b48; /* kicker, icons, logo dot */
--sans: "Sora", system-ui, sans-serif;
--safe-top: 54px;
--safe-bot: 34px;
--gutter: 20px;
--r: 12px;
--r-pill: 999px;
--bar-h: 108px; /* expanded, for body padding */
--t-fast: 160ms;
--t-bar: 280ms;
--ease: cubic-bezier(.2, .7, .2, 1);
--expo: cubic-bezier(.16, 1, .3, 1);
}
Typography
| Role | Family | Size | Weight | Line-height | Tracking | Case |
|---|---|---|---|---|---|---|
| Title | Sora | 32px | 600 | 1.12 | −0.04em | sentence |
| Price (hero) | Sora | 28px | 600 | 1 | −0.03em | |
| Price (bar) | Sora | 20px | 600 | 1 | −0.03em | |
| Logo | Sora | 16px | 600 | 1 | −0.03em | Title |
| Lede | Sora | 15px | 400 | 1.5 | 0 | sentence |
| Feature title | Sora | 14px | 600 | 1 | −0.02em | sentence |
| Button | Sora | 14px | 600 | 1 | 0 | Title |
| Bar desc | Sora | 12px | 500 | 1.4 | 0 | sentence |
| Kicker / h2 | Sora | 11/13px | 500/600 | 1 | +0.12–.14em | UPPERCASE |
One family only. Do not add a serif.
Motion
| Element | Trigger | Property | From → To | Duration | Easing |
|---|---|---|---|---|---|
| Description | compact | max-height, opacity | 36px + 1 → 0 + 0 | 280 / 160ms | --ease |
| Bar padding | compact | padding | 12px → 10px | 280ms | --ease |
| Toast | add | opacity, translateY | 0 + 8px → 1 + 0 | 160 / 280ms | --expo |
| Button | hover/active | background, scale | accent → #b84634 / 0.98 | 160ms | --ease |
| Nav border | scroll > 8 | border-color | transparent → line | 160ms | — |
Compact threshold is 72px, not intersection observer. Use { passive: true } on scroll.
States
- Bar expanded: description visible, price shows “incl. post”.
- Bar compact: description clipped, “incl. post”
display: none. Button unchanged. - Nav stuck: 1px
--linebottom border. - Add hover:
#b84634. Active: scale 0.98. - Toast
.on: visible 2200ms. - Focus-visible: 2px accent outline, 3px offset, on nav links and the button.
Accessibility
- Bar is not
role="toolbar"; it is a product CTA. The button is the control. Description is a<p>and remains in the DOM when compact (hidden visually, still in the accessibility tree — acceptable because the same facts sit in the hero). If the host AT doubles it, addaria-hidden="true"on.descwhile compact. - Toast uses
role="status"/aria-live="polite". - Mock plant is
role="img"witharia-label="Illustrated seedling in a pot". - Contrast: coral
#d4533ewith white text is about 4.6:1 at 14px/600. Ink on--bgexceeds 12:1.--ink-2on--bgis about 6:1. - Hit targets ≥ 40px. Button is 48px. Compact bar still 48px tall in the button column.
- Body
padding-top: 54pxso content never sits under lounge chrome. Barbottom: 34pxso it never sits on the home indicator.
Responsive rules
This piece is specified at 390×844. If shown elsewhere:
- 360 wide: gutters 16, title 28px, button min-width 112px. Compact threshold stays 72px.
- Tablet width: keep the bar full-bleed at the bottom; do not convert it to an inline desktop CTA. Max readable measure 40ch on the hero.
Acceptance checklist
- Frame is mobile-web 390×844; header comment says so; viewport includes
initial-scale=1. - 54px top inset and 34px bottom inset are free of fixed chrome. Nav is sticky under the 54px. Bar is fixed at
bottom: 34px. - Expanded bar shows description +
$48+ “Add to bag”. After 72px of scroll the description is gone; price and button remain. - Compact transition is 280ms on
cubic-bezier(.2,.7,.2,1)for max-height. - Add to bag shows a polite status toast for 2200ms with the $48 amount.
- Only Sora loads. Accent is coral
#d4533e, not leaf-green filled buttons (leaf is kicker/icons only). - Feature list has three items with 20px stroke SVG icons.
- Focus rings are 2px
#d4533ewith a 3px offset. - Reduced motion: compact still happens, transitions 1ms, no smooth scroll.
- Last review is not hidden behind the expanded bar (body padding-bottom includes
--bar-h).
Implementation notes
Keep the bar off the home indicator with a token, not magic numbers in four places:
:root { --safe-top: 54px; --safe-bot: 34px; --bar-h: 92px; }
body {
padding-top: var(--safe-top);
padding-bottom: calc(var(--safe-bot) + var(--bar-h) + 8px);
}
.nav { position: sticky; top: var(--safe-top); }
.bar { position: fixed; left: 0; right: 0; bottom: var(--safe-bot); }
Collapse with max-height, not display: none, so it can animate:
.bar .desc {
grid-column: 1 / -1;
max-height: 20px;
opacity: 1;
overflow: hidden;
transition: max-height 280ms var(--ease), opacity 160ms var(--ease);
}
.bar.compact .desc { max-height: 0; opacity: 0; }
Threshold on scrollY, passive listener:
addEventListener('scroll', () => {
bar.classList.toggle('compact', scrollY > 72);
nav.classList.toggle('stuck', scrollY > 8);
}, { passive: true });
Common mistakes: bottom: 0 so the bar sits on the lounge home indicator. Using position: sticky on the bar (it will not pin above the 34px inset). Two font families. A green primary button that collides with the existing Loam savings piece.
Details
- Palette
- Type
- Sora
- Motion
- Subtle motion
- Build
- One session
- Tags
Studied
StripeLet one big moving thing carry the colour. Keep the words light and the cards real.
All sourcesOn the shelves