Nº 044 of 520
Onboarding carousel
Three snap-scrolled onboarding slides with CSS-drawn illustrations, page dots that stretch into a 24px pill, and a Continue button that becomes Get started on the last slide.
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 "Onboarding carousel" (piece ios-onboarding-carousel) 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º 044 · "Onboarding carousel" · www.designlounge.live -->
# Onboarding carousel
> **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 first-run screen of "Mira", a sleep app. Three full-width slides scroll horizontally with mandatory snap; each has a 380px illustration card drawn entirely in CSS (a moon over hills, a sleep-stage bar chart that rises in, a readiness ring that sweeps to 72%), an italic-accented serif headline and one short paragraph. Below, three page dots: the active one stretches from an 8px circle to a 24px pill in the accent colour. A full-width "Continue" button advances; on the last slide its label swaps to "Get started" and "Skip" disappears. The mood is night-time and quiet: deep navy surfaces, a warm apricot accent, an old-style serif for the headings.
## Reference behaviour
1. Initial state: slide 1 ("Fall asleep on purpose") is centred, dot 1 is a 24px apricot pill, the button reads "Continue", "Skip" is top-right. The moon illustration is static with a three-ring halo.
2. Swipe or drag the track horizontally (mouse drag also works via native scroll): slides snap one at a time (`scroll-snap-stop: always`).
3. When the scroll position crosses the midpoint of a slide, the dot for that slide grows to 24px over 320ms `cubic-bezier(.32,.72,0,1)` and turns apricot; the previous one shrinks to 8px and returns to `--ink-3`.
4. Arriving on slide 2 replays its chart: 18 bars scale up from `scaleY(0)` over 700ms `cubic-bezier(.16,1,.3,1)`, staggered 40ms each.
5. Arriving on slide 3 replays the ring: the conic sweep goes 0% → 72% over 1200ms with the same expo-out curve. "Skip" hides. The button text fades out (160ms, 6px downward) and "Get started" fades in.
6. Tap "Continue": smooth-scrolls to the next slide. Tap "Get started": returns to slide 1 (the Lounge demo loops; in a product it dismisses onboarding).
7. Tap "Skip": jumps to slide 3.
8. ArrowRight / ArrowLeft anywhere move one slide.
9. Pressing the button scales it to 0.97 for 160ms.
## Structure
```
390 × 844
┌────────────────────────────────────────┐
│ (54 status) │
│ 98 Mira (italic serif 22) Skip │ header, bottom-aligned
│ ┌────────────────────────────────────┐ │
│ │ art card 334×380, r24, #161B2E │ │
│ │ ✦ ◐ moon 120 │ │
│ │ ✦ ╭────╮ ✦ │ │
│ │ ╭───────────╯ ╰──────╮ │ │ hills = large circles
│ └────────────────────────────────────┘ │
│ 34 │
│ Fall asleep on purpose (serif 36) │
│ Mira builds a wind-down for the hour │ paragraph 15/1.55, max 300
│ before bed from your own patterns… │
│ │
… 166 more lines. Copy or download for the full brief. Loading the source…Brief
206 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
The first-run screen of “Mira”, a sleep app. Three full-width slides scroll horizontally with mandatory snap; each has a 380px illustration card drawn entirely in CSS (a moon over hills, a sleep-stage bar chart that rises in, a readiness ring that sweeps to 72%), an italic-accented serif headline and one short paragraph. Below, three page dots: the active one stretches from an 8px circle to a 24px pill in the accent colour. A full-width “Continue” button advances; on the last slide its label swaps to “Get started” and “Skip” disappears. The mood is night-time and quiet: deep navy surfaces, a warm apricot accent, an old-style serif for the headings.
Reference behaviour
- Initial state: slide 1 (“Fall asleep on purpose”) is centred, dot 1 is a 24px apricot pill, the button reads “Continue”, “Skip” is top-right. The moon illustration is static with a three-ring halo.
- Swipe or drag the track horizontally (mouse drag also works via native scroll): slides snap one at a time (
scroll-snap-stop: always). - When the scroll position crosses the midpoint of a slide, the dot for that slide grows to 24px over 320ms
cubic-bezier(.32,.72,0,1)and turns apricot; the previous one shrinks to 8px and returns to--ink-3. - Arriving on slide 2 replays its chart: 18 bars scale up from
scaleY(0)over 700mscubic-bezier(.16,1,.3,1), staggered 40ms each. - Arriving on slide 3 replays the ring: the conic sweep goes 0% → 72% over 1200ms with the same expo-out curve. “Skip” hides. The button text fades out (160ms, 6px downward) and “Get started” fades in.
- Tap “Continue”: smooth-scrolls to the next slide. Tap “Get started”: returns to slide 1 (the Lounge demo loops; in a product it dismisses onboarding).
- Tap “Skip”: jumps to slide 3.
- ArrowRight / ArrowLeft anywhere move one slide.
- Pressing the button scales it to 0.97 for 160ms.
Structure
390 × 844
┌────────────────────────────────────────┐
│ (54 status) │
│ 98 Mira (italic serif 22) Skip │ header, bottom-aligned
│ ┌────────────────────────────────────┐ │
│ │ art card 334×380, r24, #161B2E │ │
│ │ ✦ ◐ moon 120 │ │
│ │ ✦ ╭────╮ ✦ │ │
│ │ ╭───────────╯ ╰──────╮ │ │ hills = large circles
│ └────────────────────────────────────┘ │
│ 34 │
│ Fall asleep on purpose (serif 36) │
│ Mira builds a wind-down for the hour │ paragraph 15/1.55, max 300
│ before bed from your own patterns… │
│ │
│ ▬ · · dots 24/8/8 │
│ ┌────────────────────────────────────┐ │
│ │ Continue (56) │ │ bottom 44
│ └────────────────────────────────────┘ │
└────────────────────────────────────────┘
<body>is a flex column:<header>98px →.track(flex 1) →<footer>..track:display:flex; overflow-x:auto; scroll-snap-type:x mandatory, scrollbar hidden,aria-roledescription="carousel"..slide:flex:0 0 100%; scroll-snap-align:center; scroll-snap-stop:always; padding:8px 28px 0. Each is a<section role="group" aria-label="n of 3">holding.art(aria-hidden),<h2>,<p>..art: 380px tall,border-radius:24px,--surfacewith a 1px--lineborder and a radial apricot glow at the bottom (::after).<footer>:padding:12px 24px 44px,.dots[role=tablist]then<button class="cta">.
Tokens
:root {
/* colour — deep navy, parchment text, apricot accent */
--bg: #0e1220;
--surface: #161b2e; /* art cards */
--surface-2: #1f2540; /* hills, light-sleep bars */
--surface-3: #1a2038; /* far hill */
--ink: #f3efe6;
--ink-2: #a9adbd; /* paragraphs, skip */
--ink-3: #6c7188; /* inactive dots, axis labels */
--line: #262c45; /* card border, ring track */
--accent: #f0c27b; /* CTA, active dot, deep-sleep bars, ring, italic words */
--accent-ink: #2b1d05; /* text on CTA */
--moon: #f6e7c6;
--rem: #8fa3d9; /* REM bars */
--glow: rgba(240,194,123,.22);
/* type */
--serif: "Instrument Serif", Georgia, serif;
--sans: "Manrope", system-ui, -apple-system, sans-serif;
/* layout */
--header-h: 98px; --art-h: 380px; --slide-pad: 28px;
--r-card: 24px; --r-btn: 16px; --cta-h: 56px;
--dot: 8px; --dot-active: 24px; --dot-gap: 6px;
--footer-bottom: 44px; /* 34 home indicator + 10 */
/* motion */
--t-micro: 160ms;
--t-dot: 320ms;
--t-hero: 700ms;
--t-ring: 1200ms;
--stagger: 40ms;
--spring: cubic-bezier(.32, .72, 0, 1);
--ease: cubic-bezier(.2, .7, .2, 1);
--expo: cubic-bezier(.16, 1, .3, 1);
}
Typography
| Role | Family | Size | Weight | Line-height | Tracking | Case |
|---|---|---|---|---|---|---|
| Brand | Instrument Serif | 22px | 400 italic | 1.2 | −0.01em | sentence |
| Headline | Instrument Serif | 36px | 400 | 1.1 | −0.015em | sentence; one <i> phrase in --accent |
| Paragraph | Manrope | 15px | 400 | 1.55 | 0 | sentence, max-width:300px |
| Skip | Manrope | 14px | 600 | 1 | 0 | sentence |
| CTA | Manrope | 16px | 700 | 1 | −0.01em | sentence |
| Big stat | Instrument Serif | 56px | 400 | 1 | −0.02em | numerals |
| Ring number | Instrument Serif | 48px | 400 | 1 | 0 | numerals |
| Stat caption | Manrope | 12px | 500 | 1.2 | +0.08em | UPPERCASE |
| Axis | Manrope | 11px | 500 | 1 | +0.06em | numerals |
Motion
| Element | Trigger | Property | From → To | Duration | Easing | Notes |
|---|---|---|---|---|---|---|
.track | CTA / keys | scrollLeft | i·width → (i±1)·width | native smooth | — | scroll-behavior via scrollTo({behavior:'smooth'}) |
.dots i | index change | width | 8px ↔ 24px | 320ms | --spring | |
.dots i | index change | background | --ink-3 ↔ --accent | 320ms | --ease | |
.bar | enter slide 2 | transform | scaleY(0) → scaleY(1) | 700ms | --expo | delay = index · 40ms, transform-origin:bottom |
.ring | enter slide 3 | --p (conic) | 0% → 72% | 1200ms | --expo | needs @property --p |
.cta span | label change | opacity / transform | 1,0 → 0,6px then back | 160ms + 160ms | --ease | text swapped at the midpoint |
.cta | :active | transform | 1 → scale(.97) | 160ms | --ease |
Reduced motion: all transitions and animations are 1ms (dots jump, bars and ring appear complete), and the track uses scroll-behavior:auto so “Continue” jumps.
States
- Active slide:
aria-current="true"on the section; its dot has classon(24px, apricot). - Last slide:
Skipgets thehiddenattribute; CTA text is “Get started”. - CTA focus-visible: 2px
--accentoutline, 3px offset. Skip focus-visible: same with 2px offset, 8px radius. - CTA active: scale 0.97.
- Dots are presentational (
role="tablist"without interactive tabs is acceptable here because the CTA and keys navigate); do not make dots the only way to move. - Loading (spec only): while fonts load, headings fall back to Georgia; keep the 36px size so layout does not jump.
Accessibility
- Track:
aria-roledescription="carousel" aria-label="Introduction"; each sliderole="group" aria-label="1 of 3"etc. .artblocks arearia-hidden="true"; the headline + paragraph carry the meaning.- Keyboard: Tab order is Skip → CTA. ArrowLeft/ArrowRight move slides from anywhere on the page. The track itself is scrollable by keyboard when focused (native).
- Contrast:
--ink-2on--bgis 8.1:1;--accent-inkon--accentis 11:1;--ink-3is only used for axis labels and inactive dots. - Hit targets: CTA 56px tall; Skip has
min-height:40px. - Announce slide changes by updating
aria-current; screen readers already read the group label when focus enters a slide.
Responsive rules
- 390 wide: art card is 334px wide (390 − 2·28).
- 360 wide: same paddings; art card 304px, height stays 380; headline drops to 32px.
- ≥ 430 wide: constrain the whole column to
max-width:420px; margin:0 autoso the art card stays under 380px wide. - Short viewports (< 760px tall): art height 300px, header 80px.
- Tablet: show the three slides side by side as static cards (no carousel), dots removed, a single “Get started” button.
Acceptance checklist
- Slides are exactly one viewport wide and snap one at a time (
scroll-snap-type:x mandatory,scroll-snap-stop:always). - Active dot is 24×8px in
#f0c27b; inactive dots are 8×8px in#6c7188; width animates over 320mscubic-bezier(.32,.72,0,1). - Index is derived from
Math.round(scrollLeft / clientWidth)on scroll, so drag, keys and buttons all update the dots. - On the last slide the CTA reads “Get started” and Skip is hidden; leaving it restores “Continue” and Skip.
- CTA text swaps via a 160ms fade-down / fade-up, not an instant change.
- Entering slide 2 replays the 18-bar rise with 40ms stagger; entering slide 3 replays the ring sweep to 72%.
- Art cards are 380px tall, radius 24px,
#161b2ewith a 1px#262c45border and a bottom apricot glow. - Headline is Instrument Serif 36px with one italic accent phrase; paragraph is Manrope 15/1.55 with a 300px measure.
- CTA is 56px, radius 16px, full width,
#f0c27bon#2b1d05text, and scales to 0.97 on press. - Footer keeps 44px clear at the bottom; header content sits below the 54px status area.
- Focus rings visible on Skip and CTA.
- Reduced motion: no bar or ring animation, dots jump, Continue jumps.
Implementation notes
Derive the index from scroll, never from what you think you did. One listener covers swipe, keys and buttons:
track.addEventListener('scroll', () => setIdx(Math.round(track.scrollLeft / track.clientWidth)));
function go(i) { track.scrollTo({ left: i * track.clientWidth, behavior: 'smooth' }); }
function setIdx(i) {
if (i === idx) return; idx = i;
dots.forEach((d, k) => d.classList.toggle('on', k === i));
const last = i === 2, label = last ? 'Get started' : 'Continue';
if (ctaText.textContent !== label) {
cta.classList.add('swap');
setTimeout(() => { ctaText.textContent = label; cta.classList.remove('swap'); }, 160);
}
skip.hidden = last;
track.children[i].querySelectorAll('.bar,.ring').forEach(el => { el.style.animation = 'none'; void el.offsetWidth; el.style.animation = ''; });
}
Animating a conic gradient needs a registered custom property; keep the ring a mask so the centre stays the card colour:
@property --p { syntax: "<percentage>"; inherits: false; initial-value: 0%; }
.ring { width: 200px; height: 200px; border-radius: 50%;
background: conic-gradient(var(--accent) 0 var(--p, 72%), var(--line) var(--p, 72%) 100%);
mask: radial-gradient(circle, transparent 78px, #000 79px);
animation: sweep 1.2s cubic-bezier(.16,1,.3,1) both; }
@keyframes sweep { from { --p: 0% } to { --p: 72% } }
Stagger via a custom property, so replaying by resetting animation does not wipe the delay:
.bar { animation: rise 700ms cubic-bezier(.16,1,.3,1) both; animation-delay: var(--d, 0ms); transform-origin: bottom; }
Common mistakes: using scroll-snap-align:start with horizontal padding on the track (slides land off-centre); hiding Skip with visibility instead of hidden (it stays in the tab order); forgetting overscroll-behavior-x:contain so a swipe on the first slide triggers browser back-navigation; not handling resize, which leaves the track between snap points.
Details
- Palette
- Type
- Instrument Serif · Manrope
- Motion
- Subtle motion
- Build
- One session
- Tags
On the shelves