Halcyon Ring sells eight hours of stillness, so the site had to move the way sleep does — slowly, softly, on a cadence you can feel in your chest. One HTML file, one stylesheet, one script. No frameworks, no three.js, no images.
A sleep wearable earns trust in two ways: it must feel calm (nothing about it should cost you adrenaline) and it must feel measured (real charts, real numbers, shown honestly). So the page splits its personality: an ambient dusk that drifts like weather and a ring that literally breathes with you, sitting above a morning report built from the patterns shipped sleep apps have already taught people to read — a score dial that shows its work, and a hypnogram of the night.
cubic-bezier(.5,.05,.15,1) at 1.3s+, everywhere. Long, symmetric,
breath-like — nothing on this page snaps.The background is a canvas rendered at 14% of the viewport size and stretched by CSS. Four radial-gradient "fields" (indigo, rose, gold, deep violet) drift on independent, very slow sine paths and are composited additively:
ctx.globalCompositeOperation = 'lighter';
for (const f of FIELDS) {
const x = (f.cx + f.dx * Math.sin(t * f.sx + f.p)) * W; // sx ≈ 0.00003
const y = (f.cy + f.dy * Math.cos(t * f.sy + f.p * 1.7)) * H;
const g = ctx.createRadialGradient(x, y, 0, x, y, f.r * Math.max(W, H));
g.addColorStop(0, `rgba(${f.c},${f.a})`);
g.addColorStop(1, `rgba(${f.c},0)`);
ctx.fillStyle = g; ctx.fillRect(0, 0, W, H);
}
The trick is the downscale: at ~185×120 pixels the gradients blur into atmosphere for free when the
browser stretches them, and the loop is throttled to ~15fps because weather doesn't need 60. With
prefers-reduced-motion it renders exactly one frame; with JavaScript off, a CSS
radial-gradient fallback on body paints the same dusk, statically.
The hero product shot is a CSS annulus — two stacked radial-gradients make the titanium torus, a masked conic-gradient adds the brushed sheen — and it doubles as a 4-7-8 breath guide, the cadence shipped by real wind-down apps. One rAF clock drives the scale, the halo, the phase label and the countdown so nothing can drift apart:
const t = ((now - start) / 1000) % 19; // 4 + 7 + 8
if (t < 4) scale = 1 + A * easeInOutSine(t / 4); // breathe in
else if (t < 11) scale = 1 + A + 0.006 * Math.sin(...); // hold (alive, barely)
else scale = 1 + A * (1 - easeInOutSine((t-11)/8)); // breathe out
ring.style.transform = `scale(${scale})`;
The animated label is aria-hidden; screen readers get one static sentence describing
the cadence instead of an announcement every four seconds. A visible "Pause the breath"
button (WCAG 2.2.2) freezes the clock and resumes exactly where it left off. Reduced motion: the ring
holds still and the label reads 4 · 7 · 8.
The night chart is an inline SVG: four stage rows (awake / REM / light / deep) and twenty
<rect> bands whose geometry was computed from a realistic 473-minute night —
deep sleep front-loaded, REM lengthening toward morning, two brief wakings. Each band carries its
chronological index as a CSS variable:
<rect class="seg sg-deep" style="--i:2" x="129.3" y="192" width="54.1" .../>
.seg { transform-box: fill-box; transform-origin: left center; }
.js .hypno .seg { transform: scaleX(0); opacity: 0; }
.js .hypno.is-in .seg {
transform: scaleX(1); opacity: 1;
transition: transform .9s var(--ease), opacity .7s var(--ease);
transition-delay: calc(var(--i) * 70ms); /* the night replays, left to right */
}
Because the SVG is complete in the markup and JavaScript only adds the is-in class,
the chart is fully drawn with scripts off, and the reduced-motion override simply
keeps every band visible. The legend's durations (1h 54m REM · 26%…) are the real totals of those
twenty rects — the chart and its caption can't disagree.
stroke-dasharray
on a rotated circle) that fills as the number counts up, above six contributor rows — the
Oura-taught pattern of never asserting a score without its evidence..js class gates every hidden state; the
hypnogram, dial, copy and layout all survive with scripts disabled.role="img" with full
sentence descriptions, decorative canvases are aria-hidden, and every interactive
element has a gold :focus-visible outline.Every path is relative and there is no build step, so deployment is three commands:
gh repo create halcyon-ring --public --source . --push
gh api --method POST /repos/USER/halcyon-ring/pages \
-f 'source[branch]=main' -f 'source[path]=/'
# live at https://USER.github.io/halcyon-ring/ in ~1 min
A .nojekyll file tells Pages to serve the folder as-is. That's the whole pipeline.
Halcyon Ring is a design-showcase concept — there is no physical ring, and last night's data is a hand-built, realistic night, not a reading. See the repository README for the full demo-vs-real map.
← Back to Halcyon Ring