← Halcyon Ring THE GUIDE · HOW IT WAS BUILT
Design showcase · build notes

A page that breathes.

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.

The idea

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.

The stack

Signature technique #1 — the flowing dusk

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.

Signature technique #2 — the breathing ring

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.

Signature technique #3 — the animated hypnogram

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.

Details that matter

Ship it on GitHub Pages

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