Nº 588 of 605
Year-in-review story cards
Six tap-through year-in-review story cards for a reading app: segmented progress, giant Anton numbers, a top five, a reading-hours ring and a share card with replay.
Preview
Use it
Free to use · credit appreciated1 Once, in your project · Node 22.20+
npx skills add SusanAcharya/Design-Lounge 2 Then ask
Using the Design Lounge skill, build "Year-in-review story cards" (piece card-wrapped-story) 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º 588 · "Year-in-review story cards" · www.designlounge.live -->
# Year-in-review story cards
> **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 end-of-year recap format, as full-screen story cards, for an invented reading app called Marginalia ("Year in pages"). Six cards play in order, each on its own flat colour field: the year, a giant page count with a monthly bar chart, a 41-day streak drawn as a calendar, the top five books, a "night owl" reading-hours ring, and a share card. A segmented progress bar at the top fills for 5.2s per card and advances automatically; tap the right 70% to skip, the left 30% to go back, hold to pause. The detail worth copying is how much it does with two typefaces and six colours: Anton at 124–200px for the one number per card, Poiret One for the sentence that makes the number personal. The chrome follows an iOS 26 language: round 40px glass-tinted icon buttons, 20px radii, spring-ish expo-out reveals.
## Reference behaviour
1. Initial state: card 1 (tomato) is on; segment 1 starts filling; "NOOR, THIS WAS YOUR" then "2026" then the sentence rise in with a 120ms stagger; twelve book spines (a 290px-tall band) grow from the bottom with a 60ms stagger.
2. When a segment finishes filling (5200ms, linear), the next card cross-fades in over 360ms and its own reveal plays.
3. Card 2: "14,382" counts up from 0 over 1400ms (quartic ease-out, starting 240ms after the card shows) and the twelve monthly bars grow; March, the peak, is full-strength lime, the rest at 50%.
4. Card 3: 41 of 56 calendar squares pop in with an 18ms stagger, starting on the fifth square, so the streak sits inside a real-looking month grid.
5. Card 4: five list rows rise in order; the number 1 is larger (64px) and tomato.
6. Card 5: "NIGHT OWL" rises, then a cream arc draws from 10pm to 1:45am on a 24-hour ring over 1200ms.
7. Card 6: the share poster (tilted −2°) and two buttons rise. The segment bar is full and auto-advance stops.
8. Tap the right 70% of the screen: next card. Tap the left 30%: previous card. ← / → keys do the same.
9. Press and hold anywhere on the tap zones for 220ms: the current segment pauses; release resumes, and the release does not count as a tap.
10. Pause button toggles the same paused state (`aria-pressed`). Replay (top-right) and "Watch again" (card 6) restart from card 1.
11. The top bar's colour, track shade and button tint switch with each card so they always read on the field.
## Structure
```
390 × 844 (status bar drawn by the Lounge, top 54px left clear)
┌──────────────────────────────────────┐
│ │ 0–54 safe area
│ ▬▬▬ ▬▬▬ ▬▬▬ ▬▬▬ ▬▬▬ ▬▬▬ │ 58: 6 segments, 3px, gap 4
│ [■] Marginalia (Ⅱ) (↺) │ 70: 32px mark, brand, 40px buttons
│ │
│ NOOR, THIS WAS YOUR (Anton 15) │ 138: card content starts
│ 2026 (Anton 152) │
│ You turned a lot of pages…(Poiret 22)│
│ │
│ ▌▐ ▌▐ ▌▐ ▌▐ ▌▐ ▌▐ spines / chart / │ margin-top:auto pushes the
│ ▌▐ ▌▐ ▌▐ ▌▐ ▌▐ ▌▐ dots / ring │ visual to the bottom
│ │ bottom padding 58
… 171 more lines. Copy or download for the full brief. Loading the source…Brief
211 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 end-of-year recap format, as full-screen story cards, for an invented reading app called Marginalia (“Year in pages”). Six cards play in order, each on its own flat colour field: the year, a giant page count with a monthly bar chart, a 41-day streak drawn as a calendar, the top five books, a “night owl” reading-hours ring, and a share card. A segmented progress bar at the top fills for 5.2s per card and advances automatically; tap the right 70% to skip, the left 30% to go back, hold to pause. The detail worth copying is how much it does with two typefaces and six colours: Anton at 124–200px for the one number per card, Poiret One for the sentence that makes the number personal. The chrome follows an iOS 26 language: round 40px glass-tinted icon buttons, 20px radii, spring-ish expo-out reveals.
Reference behaviour
- Initial state: card 1 (tomato) is on; segment 1 starts filling; “NOOR, THIS WAS YOUR” then “2026” then the sentence rise in with a 120ms stagger; twelve book spines (a 290px-tall band) grow from the bottom with a 60ms stagger.
- When a segment finishes filling (5200ms, linear), the next card cross-fades in over 360ms and its own reveal plays.
- Card 2: “14,382” counts up from 0 over 1400ms (quartic ease-out, starting 240ms after the card shows) and the twelve monthly bars grow; March, the peak, is full-strength lime, the rest at 50%.
- Card 3: 41 of 56 calendar squares pop in with an 18ms stagger, starting on the fifth square, so the streak sits inside a real-looking month grid.
- Card 4: five list rows rise in order; the number 1 is larger (64px) and tomato.
- Card 5: “NIGHT OWL” rises, then a cream arc draws from 10pm to 1:45am on a 24-hour ring over 1200ms.
- Card 6: the share poster (tilted −2°) and two buttons rise. The segment bar is full and auto-advance stops.
- Tap the right 70% of the screen: next card. Tap the left 30%: previous card. ← / → keys do the same.
- Press and hold anywhere on the tap zones for 220ms: the current segment pauses; release resumes, and the release does not count as a tap.
- Pause button toggles the same paused state (
aria-pressed). Replay (top-right) and “Watch again” (card 6) restart from card 1. - The top bar’s colour, track shade and button tint switch with each card so they always read on the field.
Structure
390 × 844 (status bar drawn by the Lounge, top 54px left clear)
┌──────────────────────────────────────┐
│ │ 0–54 safe area
│ ▬▬▬ ▬▬▬ ▬▬▬ ▬▬▬ ▬▬▬ ▬▬▬ │ 58: 6 segments, 3px, gap 4
│ [■] Marginalia (Ⅱ) (↺) │ 70: 32px mark, brand, 40px buttons
│ │
│ NOOR, THIS WAS YOUR (Anton 15) │ 138: card content starts
│ 2026 (Anton 152) │
│ You turned a lot of pages…(Poiret 22)│
│ │
│ ▌▐ ▌▐ ▌▐ ▌▐ ▌▐ ▌▐ spines / chart / │ margin-top:auto pushes the
│ ▌▐ ▌▐ ▌▐ ▌▐ ▌▐ ▌▐ dots / ring │ visual to the bottom
│ │ bottom padding 58
└──────────────────────────────────────┘
tap zones: left 0–30% = back, 30–100% = next (from y=110 down)
<main class="story" aria-roledescription="story">..topfixed:.segs(decorative) and.barwith the app mark, brand,<button id="pause" aria-pressed>,<button id="replay">.- Six
<section class="card cN" aria-label="Card N of 6">, absolutely stacked; only.onis visible. Each:.kkicker, one display element (.bigor.huge), one.psentence, and one bottom visual. - Card 4 uses an
<ol class="list">; card 6 has an<article class="poster">with a<dl>of four stats and two.btns. - Two transparent
<button class="tap">overlays:.prev(left 30%) and.next(right 70%), both fromtop:110pxso they never cover the top bar. Hidden on card 6 so its buttons work.
Card fields and content:
| # | Background | Text | Display | Visual |
|---|---|---|---|---|
| 1 | #e8442e tomato | #1a0f0a | “2026” | 12 spines, heights 46–100% |
| 2 | #0f5b3e moss | #d9f99d lime | “14,382” | 12 monthly bars + “47 books · 312 hours · 9 languages” |
| 3 | #f5c518 sun | #1b1400 | “41” | 7 × 8 calendar, squares 5–45 filled |
| 4 | #141210 night | #f4ebdd cream | — | top five list |
| 5 | #1d3fd8 cobalt | #f4ebdd | “NIGHT OWL” | 24h ring, arc 22:00–01:45 |
| 6 | #f4ebdd cream | #141210 | poster | Share to story, Watch again |
Tokens
:root {
/* card fields and their ink */
--tomato: #e8442e; --tomato-ink: #1a0f0a;
--moss: #0f5b3e; --lime: #d9f99d;
--sun: #f5c518; --sun-ink: #1b1400;
--night: #141210; --cream: #f4ebdd; --cream-2: #b9afa0; /* secondary on night */
--cobalt: #1d3fd8;
--rule: #3a342e; /* list hairline on night */
/* type */
--display: "Anton", Impact, sans-serif;
--body: "Poiret One", Futura, sans-serif;
--fs-kicker: 15px; /* Anton, +0.14em, uppercase */
--fs-big: min(39vw, 200px); /* "2026", "41" */
--fs-huge: min(30vw, 124px); /* "14,382", "NIGHT OWL" */
--fs-sentence: 22px;
/* layout */
--pad: 24px;
--content-top: 138px;
--content-bottom: 58px;
--seg-h: 3px; --seg-gap: 4px;
--r: 20px; /* poster */
--btn-h: 52px; /* share buttons, radius 26 */
--icon: 40px; /* round top-bar buttons */
/* motion */
--dur: 5200ms; /* per card */
--t-fade: 360ms;
--t-rise: 700ms; --stagger: 120ms;
--ease-out: cubic-bezier(.16, 1, .3, 1);
--ease: cubic-bezier(.2, .7, .2, 1);
}
Typography
| Role | Family | Size | Weight | Line-height | Tracking | Case |
|---|---|---|---|---|---|---|
| Kicker | Anton | 15px | 400 | 1 | +0.14em | UPPERCASE |
| Big number | Anton | min(39vw, 200px) | 400 | 0.82 | −0.01em | — |
| Huge number / word | Anton | min(30vw, 124px) | 400 | 0.86 | −0.005em | tabular, caps |
| Sentence | Poiret One | 22px | 400 | 1.28 | 0 | sentence, max 22ch |
| List title | Poiret One | 23px | 400 | 1.15 | 0 | Title |
| List author / stat label | Poiret One | 17–18px | 400 | 1.3 | 0 | sentence |
| List number | Anton | 44px (64px for #1) | 400 | 1 | 0 | — |
| Stat value | Anton | 44px | 400 | 1 | 0 | — |
| Poster headline | Anton | 56px | 400 | 0.9 | 0 | UPPERCASE |
| Button | Anton | 17px | 400 | 1 | +0.08em | UPPERCASE |
| Brand | Poiret One | 18px / 15px | 400 | 1.1 | +0.02em | Title |
Poiret One is never set below 15px; its hairline strokes vanish smaller.
Motion
| Element | Trigger | Property | From → To | Duration | Easing | Delay | Reduced motion |
|---|---|---|---|---|---|---|---|
.seg.now i | card shown | scaleX | 0 → 1 | 5200ms | linear (time) | — | no auto-advance; tap only |
.card | show / hide | opacity (+ visibility) | 0 ↔ 1 | 360ms | --ease | — | instant |
.rise | card shown | opacity, translateY | 0, 28px → 1, 0 | 700ms | --ease-out | 120ms + i × 120ms | shown, no move |
.spines i | card 1 | scaleY | 0 → 1 | 900ms | --ease-out | 400ms + i × 60ms | full height |
| count-up | card 2 | text | 0 → 14,382 | 1400ms | quartic out | 240ms | final value |
.months i | card 2 | scaleY | 0 → 1 | 800ms | --ease-out | 500ms + i × 50ms | full height |
.dots i.f | card 3 | scale, opacity | .3, 0 → 1 | 380ms | --ease-out | 500ms + i × 18ms | shown |
ring .arc | card 5 | stroke-dashoffset | 1 → 0 | 1200ms | --ease-out | 600ms | drawn |
.btn | active | scale | 1 → .97 | 160ms | --ease | — | none |
States
- Playing: current segment filling; previous segments full; later segments show only the track shade.
- Paused (hold or button):
.pausedon the story setsanimation-play-state: pausedon the current segment. The pause button reads “Resume story” while pressed. - Last card: segment full, no auto-advance, tap zones hidden so the buttons are reachable.
- Per-card chrome: the top bar’s text, segment track (
rgbaof the field’s ink at 25–60%) and button tint (ink at 8–16%) change on every card. - Focus-visible: 2px outline in the current text colour, 3px offset, on every button.
- Button pressed: scale .97.
Accessibility
- Each card is a
<section>witharia-label="Card N of 6"; hidden cards getaria-hidden="true"andvisibility:hidden, so only the current card is read. - Tap zones are real buttons labelled “Previous card” and “Next card”; ← / → work from anywhere.
- The pause control is a toggle button with
aria-pressed; it is the WCAG 2.2.2 escape hatch for auto-advancing content. - The progress segments, spines, bars, calendar and ring are decorative (
aria-hidden); their facts are in the sentence beside them. - Contrast on every field: tomato ink 4.7:1, lime on moss 7.0:1, sun ink 11.2:1, cream on night 15.8:1, cream on cobalt 6.5:1,
--cream-2on night 8.6:1. - Hit targets: icon buttons 40 × 40; share buttons 52px tall, full width.
- Reduced motion: nothing auto-advances; every visual is shown complete.
Responsive rules
- 390 × 844: as specified.
- 360 wide: display sizes follow the
vwclamps (“2026” at 140px, “14,382” at 108px); the sentence keeps 22px and wraps to four lines. - Short phones (< 700px tall): drop
--content-topto 120px and hide the card 2 stats row. - Tablet or desktop: centre a 390 × 844 story column on a
#141210ground with 20px radius; tap zones stay inside the column.
Acceptance checklist
Always
- Six segments at the top, 3px tall, 4px gaps; the current one fills linearly over 5200ms and advances on
animationend. - Right 70% taps go forward, left 30% go back, ← / → mirror them.
- Holding for 220ms pauses; the release does not advance.
- A pause toggle and a replay control are always visible; the last card has a “Watch again” button.
- Each card shows one display number or word and one sentence, no more.
- Only the visible card is in the accessibility tree.
- Every text colour passes 4.5:1 on its card field.
- Reduced motion: no auto-advance, no rise, visuals complete.
This demo
- Card order: 2026 (tomato), 14,382 pages (moss), 41-day streak (sun), top five (night), NIGHT OWL (cobalt), share (cream).
- The monthly bars sum to 14,382 and March (2,104) is the peak.
- Calendar squares 5 to 45 of 56 are filled (Mar 3 to Apr 12).
- The ring arc runs from 22:00 to 01:45 with 12AM at the top.
- Top five: The Salt Orchard, A Field Guide to Leaving, Nine Small Fires, Winter Ferry, The Glass Cartographer.
Implementation notes
Let CSS own the clock. The segment’s own animation is the timer; JS only listens for its end, so pausing is a single class:
.seg i { position:absolute; inset:0; background:currentColor; transform:scaleX(0); transform-origin:0 50%; }
.seg.done i { transform:scaleX(1); }
.seg.now i { animation: fill var(--dur) linear forwards; }
.paused .seg.now i { animation-play-state: paused; }
@keyframes fill { to { transform: scaleX(1); } }
segs.addEventListener('animationend', () => { if (!story.classList.contains('paused')) show(idx + 1); });
// restart the same segment when replaying: kill, reflow, restore
const i = segs.children[idx].firstChild; i.style.animation = 'none'; void i.offsetWidth; i.style.animation = '';
Hold without a phantom tap. Swallow the click that follows a hold in the capture phase:
let hold = 0, held = false;
story.addEventListener('pointerdown', (e) => { if (e.target.closest('.tap')) hold = setTimeout(() => { held = true; story.classList.add('paused'); }, 220); });
story.addEventListener('pointerup', () => { clearTimeout(hold); if (held) story.classList.remove('paused'); });
story.addEventListener('click', (e) => { if (held) { held = false; e.stopPropagation(); } }, true);
Draw the clock arc as a path, not a dashed circle. Compute both ends from the hour (θ = h / 24 × 360°, measured clockwise from the top) and animate stroke-dashoffset with pathLength="1": 10pm is (12.05, 6.23) and 1:45am is (27.03, 5.74) on r = 15.9 around (20, 20).
Common mistakes: letting display numbers overflow at 390 (clamp them with vw); tap zones covering the top bar, which makes pause and replay unreachable; leaving all six cards in the accessibility tree; animating width on the segments instead of scaleX.
Details
- Palette
- Type
- Anton · Poiret One
- Motion
- Rich motion
- Build
- One session
- Tags
Studied
MindMarketOne path ties the page together. Cards sit beside it, and the numbers pile up as you scroll.
All sources