Design Lounge
Install

Nº 668 of 677

Kids bedtime story shelf

A bedtime shelf of four SVG-drawn covers, read-to-me or I'll-read, big story text where each word lights as it is read, swipe page turns and a calm The end.

Preview

Open full size
390 × 844 · 20.3 KB · 194 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 "Kids bedtime story shelf" (piece kids-story-shelf) 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

205 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 bedtime corner of a fictional kids reading app, “Lanternlight”, for ages 4 to 8. Three screens: a shelf of four big covers drawn in flat SVG (a lighthouse at night, an owl on a branch, a hedgehog in a red hat, the moon and a pink sock), a story page with one picture and two or three short sentences in 30px Lora, and a quiet “The end”. A grown-up or child picks “Read to me” (the device reads aloud and each word lights up on a soft yellow plate as it is spoken) or “I’ll read” (silent, and tapping any word says just that word). Night mode is the default: deep ink-navy with cream text, so the screen is not a lamp in a dark room. The detail worth copying: the highlight follows the real voice using speechSynthesis boundary events, and falls back to a per-word timer when the browser gives none, so the karaoke never stalls.

Language: iOS 26-ish (64–88px round controls, 24–28px radii, soft lift shadows, crossfade and short slide page turns).

Reference behaviour

  1. First frame: the shelf in night mode. Title “Story shelf” (Lora 30px) with a 64px moon button (filled moon = night on). A 2 × 2 grid of covers 173 × 284: picture on top (212px), title on a solid plate (72px). The lighthouse cover has a coral bookmark ribbon (it is mid-story, page 2 of 4). At the bottom, two 76px pill choices: “Read to me” (selected, moon-yellow fill) and “I’ll read” (2px outlined).
  2. Lights in the pictures move slowly: the lighthouse beam sweeps ±12° over 6s, waves bob 4px over 4s, stars twinkle 0.45 → 1 opacity over 3s.
  3. Tap a cover: the shelf fades out and the story fades and scales in (320ms fade, 480ms scale 0.97 → 1). The lighthouse opens at page 2; the others at page 1. Focus moves to the play button.
  4. Read to me: reading starts at once (the cover tap is the gesture). The play button becomes Pause. Each word gets the .on plate as the voice reaches it. After the last word: the highlight clears, the next arrow nudges 6px (1.4s loop), and 1500ms later the page turns by itself.
  5. If no boundary event arrives within 700ms of speak() (or speechSynthesis is missing or throws), a timer drives the highlight: each word stays for 280ms + 55ms × its length. Page done fires when the timer passes the last word, or on onend if boundary events were working.
  6. Pause stops the voice (cancel()) and clears the highlight; Play reads the page from its start.
  7. I’ll read: no voice on page open. Tapping a word lights it and says only that word (rate 0.8); the plate clears after 900ms.
  8. Page turns: big 76px arrows, a horizontal swipe over 60px anywhere on the page (not on a button), or ArrowLeft/ArrowRight. New text slides in 36px from the turn direction (480ms expo out). Dots under the text show the page; the current dot widens to 28px. Previous is disabled on page 1.
  9. After the last page: “The end” screen with a crescent moon on a navy disc, “The end” in Lora italic 60px, “Sleep tight, Juno. Lumo is sleeping too.” (or “Sleep tight, Juno.”), and two 72px buttons: “Read again” (primary, focused) and “Back to shelf”. No stars, scores or streaks.
  10. Night button on any screen swaps the whole palette (600ms colour transition) and updates aria-pressed on both buttons.
  11. Back (64px chevron) or Escape returns to the shelf and stops the voice.

Structure

390 × 844, night #141C33, column padding 54 / 16 / 34
SHELF                                   STORY
┌──────────────────────────────────┐    ┌──────────────────────────────────┐
│ 54px clear                       │    │ 54px clear                       │
│ Story shelf               ( ☾ )  │    │ ( ‹ )  The Sleepy Lighthouse (☾) │ 64h
│ ┌────────────┐ ┌────────────┐    │    │ ┌──────────────────────────────┐ │
│ │ lighthouse ▮│ │   owl      │   │    │ │  scene 358 × 262, r 28       │ │
│ │            │ │            │    │    │ └──────────────────────────────┘ │
│ │ The Sleepy │ │ Otto Counts│    │    │ One foggy night, a [little]      │ Lora 30/1.45
│ └────────────┘ └────────────┘    │    │ boat was lost. "Over here!"      │ min 220h
│ ┌────────────┐ ┌────────────┐    │    │ called Lumo, and he shone…       │
│ │ hedgehog   │ │ moon, sock │    │    │            ○ ━ ○ ○               │ dots 20h
│ └────────────┘ └────────────┘    │    │  ( ‹ )      ( ❚❚ )      ( › )    │ 96h: 76 / 88 / 76
│ [ 🔈 Read to me ] [ 📖 I'll read ]│    │ 34px clear                       │
└──────────────────────────────────┘    └──────────────────────────────────┘
covers 173 × 284, gap 12, picture 212, title plate 72
  • Three section.view layers, fixed and stacked; the hidden ones get .off (opacity 0, visibility:hidden) and inert.
  • Shelf: h1, button.nightBtn[aria-pressed], div.grid of button.cover (each with an svg using a shared <symbol> and a span.t), div.mode[role=radiogroup] with two role=radio buttons.
  • Story: back button, div.mini title, night button, div.scene (same symbol, larger), p.words with one span per word, div.dots, div.ctrl with prev, play, next.
  • End: moon svg, h2, p, two buttons.
  • One div[aria-live=polite] announces each page’s text, “The end.” and night mode changes.

Tokens

:root {                       /* day */
  --bg: #EAF0E6;              /* sage linen page */
  --surf: #FBF8F1;            /* title plates, round buttons */
  --well: #DCE5D6;            /* page dots, moon disc */
  --text: #1F2A3D;
  --text-2: #46536A;
  --hl: #FFE08A;              /* word plate */
  --hl-text: #1F2A3D;
  --accent: #2F6F6A;          /* play, selected choice, current dot */
  --accent-text: #FFFFFF;
  --edge: rgba(31,42,61,.10);
  --ribbon: #E2574C;
  /* type */
  --serif: "Lora", Georgia, serif;
  --sans: "Karla", system-ui, sans-serif;
  --fs-story: 30px; --lh-story: 1.45; --fs-end: 60px; --fs-h1: 30px; --fs-cover: 17px; --fs-choice: 18px; --fs-mini: 15px;
  /* shape */
  --r-cover: 24px; --r-scene: 28px; --r-word: 10px; --r-pill: 999px;
  --hit: 64px; --arrow: 76px; --play: 88px; --choice-h: 76px; --big-h: 72px;
  --lift: 0 10px 26px rgba(20,28,51,.16);
  /* motion */
  --ease: cubic-bezier(.2,.7,.2,1);
  --expo: cubic-bezier(.16,1,.3,1);
  --sheet: cubic-bezier(.32,.72,0,1);
  --t-micro: 160ms; --t-layout: 320ms; --t-page: 480ms; --t-theme: 600ms;
  --word-base: 280ms; --word-per-char: 55ms; --auto-turn: 1500ms; --boundary-wait: 700ms;
}
:root[data-night] {           /* default */
  --bg: #141C33; --surf: #1E2946; --well: #28355A;
  --text: #F3EBDD; --text-2: #C9C3D6;
  --hl: #F6D27A; --hl-text: #141C33;
  --accent: #F6D27A; --accent-text: #141C33;
  --edge: rgba(243,235,221,.10);
  --lift: 0 10px 26px rgba(0,0,0,.35);
}

Typography

RoleFamilySizeWeightLine-heightNotes
Story textLora30px5001.45−0.005em, ≤ 22 words a page
The endLora italic60px5001−0.02em
End lineLora22px5001.35--text-2
Shelf titleLora30px6001−0.01em
Cover titleLora17px6001.2two lines max
Choices, buttonsKarla18–20px8001nowrap
Story title (bar)Karla15px7001.2ellipsis

Lora’s open counters and moderate contrast hold up at 30px for early readers; Karla carries the controls so the two never compete.

Motion

WhatTriggerPropertyFrom → toDuration / easingReduced motion
Screen swapcover, back, endopacity, scale0 / .97 → 1 / 1320ms --ease, 480ms --sheetopacity only
Page textturntranslateX, opacity±36px / 0 → 0 / 1480ms --expo200ms fade
Word plateeach wordbackground, colournone → --hl160ms --easesame
Next nudgepage donetranslateX0 → 6px → 01.4s loopnone
Current dotturnwidth10 → 28px320ms --easesame
Beamalwaysrotate−14° → 10°6s alternatestill
WavesalwaystranslateY0 → 4px4s alternatestill
Starsalwaysopacity.45 → 13s alternatestill
Night modetogglecoloursday ↔ night600ms --easesame

States

  • Cover pressed: scale 0.97.
  • Choice selected: accent fill and text in --accent-text; unselected has a 2px --text-2 outline (both above 3:1 on the page).
  • Reading: play shows Pause bars, label “Pause reading”; one word has the plate.
  • Page done: next arrow nudges; on the last page its label is “Finish story”.
  • Prev disabled: on page 1, icon in --text-2.
  • Night on: moon icon filled; off: outline only.
  • Focus-visible: 4px --accent outline, 4px offset.

Accessibility

  • Covers are buttons named by the title (“The Sleepy Lighthouse, page 2 of 4”).
  • Read mode is a radiogroup; ArrowLeft/ArrowRight switch it.
  • Story keyboard: ArrowLeft/Right turn pages, Escape goes back, Tab reaches back, night, play, prev, next.
  • The polite live region speaks each page’s text for screen-reader users; the visual highlight is not announced word by word.
  • Speech only starts from a tap (cover, play, word, page turn), inside try/catch, and is silently skipped if missing.
  • Contrast: night cream #F3EBDD on #141C33 14:1; highlight #141C33 on #F6D27A 11.6:1; day text #1F2A3D on #EAF0E6 12:1; --text-2 above 4.5:1 in both modes. Titles sit on solid plates, never on the pictures.
  • Hit targets: 64px bar buttons, 76px arrows, 88px play, 76px choices, 72px end buttons, words are at least 44px tall at 30px × 1.45.

Responsive rules

  • At 360 wide: covers 158 × 270, story text 28px, arrows 72px; the text block keeps 220px min height so controls never jump.
  • Taller than 844: extra height goes above the dots; controls stay in the bottom 130px.
  • At tablet width: the shelf becomes 3 columns of 220 × 320 covers inside a 720px column; the story page puts the picture left and text right at 34px.

Acceptance checklist

Always

  • Covers are drawn (SVG or CSS), titles on solid plates, at least 150px wide.
  • Read-to-me highlights the spoken word from onboundary charIndex, mapped to word spans.
  • With no boundary event in 700ms, a timer highlights words at 280ms + 55ms per character.
  • speechSynthesis calls are in try/catch; the page still works without it.
  • Read-to-me turns the page 1500ms after the last word; I’ll-read never speaks unless a word is tapped.
  • Swipe over 60px, the arrows and ArrowLeft/Right all turn pages.
  • Hidden screens are visibility:hidden and inert.
  • Night mode is the default and recolours every surface, including the moon icon fill.
  • The end screen has no score; Read again restarts from page 1.
  • document.documentElement.scrollWidth is 390; reduced motion stops beam, waves, twinkle and slides.

This demo

  • Books: The Sleepy Lighthouse (4 pages, opens at page 2), Otto Counts the Stars, A Hat for Hedgehog, The Moon’s Lost Sock (3 pages each).
  • Child’s name Juno in “Sleep tight, Juno.”
  • Night palette #141C33 / #F3EBDD / #F6D27A; day #EAF0E6 / #1F2A3D / #2F6F6A.

Implementation notes

Map boundary charIndex to a word. Build the utterance from the spans so the offsets line up, and keep each span’s start index:

const ws = [...wordsEl.children], starts = []; let c = 0;
ws.forEach(s => { starts.push(c); c += s.textContent.length + 1; });
const u = new SpeechSynthesisUtterance(ws.map(s => s.textContent).join(' '));
u.rate = .82;
u.onboundary = e => {
  if (e.name === 'sentence') return; got = true;
  let k = 0; while (k + 1 < starts.length && starts[k + 1] <= e.charIndex) k++;
  highlight(k);
};
u.onend = () => { if (got) pageDone(); };
speechSynthesis.speak(u);
setTimeout(() => { if (!got) timed(0); }, 700);

Guard every async callback with a token. Turning a page, pausing or leaving must kill the old voice and the old timers, or a late onend will turn the next page:

function stop() { token++; clearTimeout(timer); try { speechSynthesis.cancel(); } catch (e) {} }
function timed(t, i) {
  if (t !== token) return;
  if (i >= ws.length) return pageDone(t);
  highlight(i);
  timer = setTimeout(() => timed(t, i + 1), 280 + ws[i].textContent.length * 55);
}

Highlight with a background, not a weight change. Bolding the spoken word reflows the line on every word. A padded background with equal negative margin (padding:0 3px; margin:0 -3px) lights the word without moving anything. Common mistakes: starting speech on page load (blocked, and not the child’s choice), letting the timed fallback and onend both call page done, and putting title text straight onto the cover picture.

Details

Palette
Type
Lora · Karla
Motion
Subtle motion
Build
Multi-step
Designed bySusan AcharyaKathmandu

Studied

Tailwind CSS docs

Put each instruction next to its code. Draw the grid with thin lines and let the empty sides carry a faint hatch.

All sources

Install once. Ask for anything.

The MCP server gives your agent 600+ pieces, the themes and the review tools. It installs the skill too. Free, no account.

claude mcp add -s user design-lounge -- npx -y design-lounge-mcp@latest

Claude Code. For Cursor, Codex, VS Code, ChatGPT and the skill alone, see Install.