Design Lounge
Install

Nº 665 of 677

Nocturne sleep stages chart

A dark sleep screen: a scrubbable hypnogram of awake, REM, light and deep, a stage ring, seven nights of stacked bars and one plain-language insight.

Preview

Open full size
390 × 844 · 16.0 KB · 207 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 "Nocturne sleep stages chart" (piece phone-chart-sleep-stages) 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

197 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 morning screen of Nocturne, a fictional sleep tracker, for the night of Thursday 5 to Friday 6 November. It is dark first and calm: deep navy cards, one cool-to-warm stage scale (deep blue, light periwinkle, aqua REM, apricot awake) and every number set in Chivo Mono so times and durations line up. Four blocks: a hypnogram you can scrub with a finger, a stage ring with totals, seven nights as stacked bars, and one sentence that says what the night meant. The detail worth copying: the stage colours run from dark to light as sleep gets lighter, so the ring, the bars and the hypnogram rows all read the same way without a legend, and tapping a stage in the legend dims everything else in both the hypnogram and the ring.

Reference behaviour

  1. First frame: last night, 7h 25m asleep, in bed 23:08 to 06:53. The scrub sits in the middle of the first deep stretch: the readout reads “00:04 · Deep sleep · 23:46–00:21 · 35 min” and that segment has a 1.5px light outline.
  2. Hypnogram: four rows (Awake, REM, Light, Deep, top to bottom, 37px apart). Each stretch is a 14px-tall rounded bar on its row, coloured by stage; thin vertical connectors (#3A4870, 1.5px) join consecutive stretches. Hour grid lines every hour, axis labels every two hours.
  3. Pointer down anywhere on the plot seeks there and captures the pointer; moving keeps scrubbing. A dashed hairline and a 14px handle (light disc with a 3px navy ring) sit on the active stretch. The readout above the plot updates every move: clock, stage, stretch range and length.
  4. Ring: 112px donut, 13px stroke, arcs in the order Deep, Light, REM, Awake with 3px gaps, starting at 12 o’clock. The centre reads the time asleep (in-bed minus awake) and “asleep · 96%” (sleep efficiency).
  5. Legend: four rows (Deep, Light, REM, Awake) with duration and share. Tapping one sets a focus: its row fills, other hypnogram bars fade to 22% opacity, other ring arcs to 25%. Tapping it again clears the focus.
  6. Seven nights: Sat 31 to Fri 6, one stacked bar per night (20px wide, deep at the bottom, awake on top), height proportional to time in bed (9h = 72px). A dashed goal line marks 7h 30m. The selected night’s day label sits on a light pill.
  7. Tapping a night switches the whole screen to that night: header, hypnogram, ring, legend and insight; the kicker changes from “Last night” to “Mon night”.
  8. Insight: one sentence built from the data, for example “1h 48m of deep sleep, 10m more than your week. 74% of it came before 02:00, and you woke once in the night.” It fades up (opacity .2 → 1, 4px, 360ms) each time it changes.

Structure

390 × 844, navy ground, cards inset 16
┌──────────────────────────────────────┐
│ 54px clear                           │
│ NOCTURNE · LAST NIGHT        7h25m   │ kicker 11px mono; total 34px mono
│ Thu 5 to Fri 6 November  in bed …    │ 15px / 600; 12px mono
│ ┌──────────────────────────────────┐ │ hypnogram card, r 20, pad 12
│ │ ■ 00:04 Deep sleep  23:46–00:21… │ │ readout 40h
│ │ Awake  ▬    ┊         ▮      ▮   │ │ plot 140h, labels in first 50px
│ │ REM       ▬ ┊   ▬    ▬▬   ▬▬  ▬▬ │ │ rows 37px apart, bars 14h
│ │ Light  ▬ ▬ ▬┊ ▬  ▬ ▬  ▬ ▬  ▬ ▬   │ │
│ │ Deep    (●)━━  ━━  ▬             │ │ handle on the active bar
│ │        00:00  02:00  04:00 06:00 │ │ axis 11px mono
│ └──────────────────────────────────┘ │
│ ┌──────────────────────────────────┐ │ totals card: grid 112px 1fr
│ │ (ring)    ■ Deep   1h 48m  23%   │ │ legend rows 34h
│ │ 7h 25m    ■ Light  3h 23m  44%   │ │
│ │ asleep    ■ REM    2h 14m  29%   │ │
│ │           ■ Awake     20m   4%   │ │
│ └──────────────────────────────────┘ │
│ ┌──────────────────────────────────┐ │ week card
│ │ Seven nights         Goal 7h 30m │ │
│ │ ┄┄┃┄┄┃┄┄┃┄┄┃┄┄┃┄┄┃┄┄┃┄┄┄┄┄┄┄┄┄┄ │ │ 7 × stacked bars 20w, 104 area
│ │ Sat31 Sun1 … Thu5 [Fri 6]        │ │ labels 22h
│ └──────────────────────────────────┘ │
│ ┌──────────────────────────────────┐ │ insight card, 15px / 1.42
│ │ ☾ 1h 48m of deep sleep, 10m more │ │
│ └──────────────────────────────────┘ │
└──────────────────────────────────────┘
  • header: kicker, h1 with the night, total and in-bed range.
  • section.hyp[aria-label]: aria-hidden readout, div.plot (pointer surface) with an aria-hidden SVG and div.handle[role=slider][tabindex=0], axis.
  • section.tots[aria-label]: ring SVG (aria-hidden) with an HTML centre, ul.legend of four button[aria-pressed].
  • section.week: heading, goal text, div.bars with seven button.night[aria-pressed].
  • section.ins[aria-live=polite] with an icon and one p.

Tokens

:root {
  --bg: #0B1120;       /* screen */
  --card: #121A2E;     /* cards */
  --card-2: #18223A;   /* focused legend row */
  --line: #243150;     /* card border */
  --grid: #1C2741;     /* row and hour lines, ring track */
  --link: #3A4870;     /* hypnogram connectors */
  --goal: #5C6A8C;     /* dashed goal line */
  --text: #E8ECF6;
  --text-2: #9AA6C2;   /* 7:1 on --card */
  /* stage scale: dark = deep, light = awake */
  --deep: #4F6BD0;
  --light: #7E9BEA;
  --rem: #6FD3C1;
  --awake: #F3B27A;
  --sans: "Chivo", system-ui, sans-serif;
  --mono: "Chivo Mono", ui-monospace, monospace;
  --fs-total: 34px; --fs-ring: 18px; --fs-read: 15px; --fs-title: 15px; --fs-insight: 15px;
  --fs-body: 14px; --fs-mono-sm: 12px; --fs-label: 11px;
  --r-card: 20px; --r-row: 10px; --r-bar: 4px; --r-stack: 5px;
  --row-gap: 37px; --bar-h: 14px; --handle: 14px; --ring: 112px; --ring-stroke: 13px; --stack-w: 20px;
  --ease: cubic-bezier(.2,.7,.2,1);
  --expo: cubic-bezier(.16,1,.3,1);
  --t-dim: 260ms; --t-ring: 600ms; --t-insight: 360ms; --t-hover: 200ms;
}

Typography

RoleFamilySizeWeightLine-heightTracking
Total asleepChivo Mono34px, units 17px in --text-25001−0.03em
Ring centreChivo Mono18px5001−0.02em
Readout clockChivo Mono15px50010
Night titleChivo15px6001.40.01em
InsightChivo15px (lead 600)4001.420
Stage name, card headingChivo14px500–6001.40
DurationsChivo Mono13px50010
Ranges, shares, axis, daysChivo Mono11–12px40010
KickerChivo Mono11px50010.12em, upper

Motion

WhatTriggerFrom → toDuration / easingReduced motion
Scrubpointer, keysfollows inputnonesame
Stage focus (bars)legend tapopacity 1 → .22260ms easeinstant
Stage focus (ring)legend tapopacity 1 → .25260ms easeinstant
Ring arcsnight changedasharray and offset600ms expoinstant
Insightnight changeopacity .2, y 4px → rest360ms easeinstant
Night bar hoverhoverscaleY 1.03200ms easenone

No looping motion: this is a screen read half awake.

States

  • Default: all stages at full opacity, scrub on the first deep stretch.
  • Stage focused: the legend row filled --card-2, aria-pressed=true; other stages dimmed in the plot and ring.
  • Active stretch: 1.5px --text outline on the bar under the scrub.
  • Selected night: day label on a --text pill with --bg text; others --text-2.
  • Not last night: kicker reads “Nocturne · Mon night”.
  • Focus-visible: 2px --text outline, 3px offset; round on the handle.

Accessibility

  • The handle is role="slider" in minutes since bedtime with aria-valuetext like “00:04, Deep, 35 minute stretch from 23:46”. Keys: ← → 5 minutes, ↑ ↓ 30 minutes, Shift × 6, PageDown / PageUp next or previous stretch, Home, End.
  • The SVG and the visual readout are aria-hidden; the slider value text is the accessible readout.
  • Legend buttons say the value and the action (“REM 2h 14m, 29 percent. Highlight in chart”); night buttons say the full date and time asleep.
  • Stage changes and night changes are announced in polite live regions; the insight card is itself polite.
  • Contrast: --text-2 on cards is 7:1. Stage colours carry no text; every stage is also named in the readout and legend, so colour is never the only cue.
  • Hit targets: legend rows full width × 34 inside a 14px-padded card; night buttons about 46 × 104; the whole plot is the scrub surface.

Responsive rules

  • Column max 430px, centred; the hypnogram reads its width on load and on resize, so bars stay proportional to minutes.
  • At 360 wide the readout range (“23:46–00:21 · 35 min”) stays on one line at 12px; axis labels stay every two hours.
  • If the screen is shorter than 800px, let the page scroll; the hypnogram is first and fully visible.
  • On tablet, put the hypnogram full width and the ring, week and insight in three columns below it.

Acceptance checklist

Always

  • Four hypnogram rows in the order Awake, REM, Light, Deep, with connectors between stretches.
  • Stage colours step from dark (deep) to light (awake) and match across hypnogram, ring, legend and bars.
  • Scrubbing updates the readout, hairline, handle and active-stretch outline on every move.
  • The ring shows four arcs with gaps and the time asleep plus efficiency in the centre.
  • Legend tap dims other stages in both the plot and the ring; tapping again clears it.
  • Seven stacked bars scaled to time in bed, a dashed goal line, the selected night marked.
  • Choosing a night redraws every block from that night’s data.
  • The insight is one plain sentence computed from the data, not a canned line.
  • Slider is keyboard operable with readable value text; scroll width 390.
  • Reduced motion keeps everything except fades and arc tweens.

This demo

  • Nocturne, Thu 5 to Fri 6 November, in bed 23:08 to 06:53, 7h 25m asleep, 96%.
  • Deep 1h 48m (23%), Light 3h 23m (44%), REM 2h 14m (29%), Awake 20m (4%).
  • Nights Sat 31 to Fri 6, goal 7h 30m.

Implementation notes

Model the night as stretches, draw everything from them. One array of { s, a, b } (stage, start minute, end minute) feeds the hypnogram, the totals, the ring, the week bars and the insight:

const tot = (n) => { const t = [0, 0, 0, 0]; n.segs.forEach(g => t[g.s] += g.b - g.a); return t; };
const X = (m, n, W) => L + (m - n.start) / (n.end - n.start) * (W - L - 4);
n.segs.forEach((s, i) => {
  const x = X(s.a, n, W), w = Math.max(2, X(s.b, n, W) - x);
  g += `<rect class="seg" data-i="${i}" x="${x}" y="${TOP + s.s * ROW - 7}" width="${w}"
          height="14" rx="${Math.min(4, w / 2)}" fill="${ST[s.s][1]}"/>`;
});

Clamp the radius to half the width: two-minute awakenings are 2px wide and a 4px radius would turn them into nothing.

Ring arcs with gaps. Use one circle per stage with stroke-dasharray and a running offset; subtract the gap from each arc, not from the circumference:

const C = 2 * Math.PI * 54; let off = 0;
[3, 2, 1, 0].forEach(k => { const len = t[k] / all * C;
  g += `<circle r="54" cx="64" cy="64" stroke="${ST[k][1]}"
          stroke-dasharray="${Math.max(0, len - 3)} ${C}" stroke-dashoffset="${-off}"/>`;
  off += len; });

The insight is arithmetic, not copy. Compare the night’s deep sleep to the seven-night mean, then measure how much of it fell before 02:00 (minute 1560 counting from the previous midnight) and count awakenings that are not the first or last stretch. Say “right on your weekly average” when the difference is under 4 minutes.

Put role="slider" on the small HTML handle with pointer-events:none, and let the plot take the pointer, so the control has a visible edge and drags start anywhere. Common mistakes: using a rainbow of unrelated stage colours, drawing the hypnogram as a smoothed line (stages are discrete), and placing labels on the coloured bars.

Details

Palette
Type
Chivo · Chivo Mono
Motion
Subtle motion
Build
Multi-step
Designed bySusan AcharyaKathmandu

Studied

Abel

A control. It studies this site for quiet, not for a show.

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.