Design Lounge
Install

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

Open full size
390 × 844 · 17.0 KB · 226 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 "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.

Brief

211 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 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
└──────────────────────────────────────┘
tap zones: left 0–30% = back, 30–100% = next (from y=110 down)
  • <main class="story" aria-roledescription="story">.
  • .top fixed: .segs (decorative) and .bar with 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 .on is visible. Each: .k kicker, one display element (.big or .huge), one .p sentence, 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 from top:110px so they never cover the top bar. Hidden on card 6 so its buttons work.

Card fields and content:

#BackgroundTextDisplayVisual
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#141210posterShare 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

RoleFamilySizeWeightLine-heightTrackingCase
KickerAnton15px4001+0.14emUPPERCASE
Big numberAntonmin(39vw, 200px)4000.82−0.01em—
Huge number / wordAntonmin(30vw, 124px)4000.86−0.005emtabular, caps
SentencePoiret One22px4001.280sentence, max 22ch
List titlePoiret One23px4001.150Title
List author / stat labelPoiret One17–18px4001.30sentence
List numberAnton44px (64px for #1)40010—
Stat valueAnton44px40010—
Poster headlineAnton56px4000.90UPPERCASE
ButtonAnton17px4001+0.08emUPPERCASE
BrandPoiret One18px / 15px4001.1+0.02emTitle

Poiret One is never set below 15px; its hairline strokes vanish smaller.

Motion

ElementTriggerPropertyFrom → ToDurationEasingDelayReduced motion
.seg.now icard shownscaleX0 → 15200mslinear (time)—no auto-advance; tap only
.cardshow / hideopacity (+ visibility)0 ↔ 1360ms--ease—instant
.risecard shownopacity, translateY0, 28px → 1, 0700ms--ease-out120ms + i × 120msshown, no move
.spines icard 1scaleY0 → 1900ms--ease-out400ms + i × 60msfull height
count-upcard 2text0 → 14,3821400msquartic out240msfinal value
.months icard 2scaleY0 → 1800ms--ease-out500ms + i × 50msfull height
.dots i.fcard 3scale, opacity.3, 0 → 1380ms--ease-out500ms + i × 18msshown
ring .arccard 5stroke-dashoffset1 → 01200ms--ease-out600msdrawn
.btnactivescale1 → .97160ms--ease—none

States

  • Playing: current segment filling; previous segments full; later segments show only the track shade.
  • Paused (hold or button): .paused on the story sets animation-play-state: paused on 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 (rgba of 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> with aria-label="Card N of 6"; hidden cards get aria-hidden="true" and visibility: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-2 on 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 vw clamps (“2026” at 140px, “14,382” at 108px); the sentence keeps 22px and wraps to four lines.
  • Short phones (< 700px tall): drop --content-top to 120px and hide the card 2 stats row.
  • Tablet or desktop: centre a 390 × 844 story column on a #141210 ground 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
Designed bySusan AcharyaKathmandu

Studied

MindMarket

One path ties the page together. Cards sit beside it, and the numbers pile up as you scroll.

All sources