MERIDIAN← Back to site
Build guide · Site 26 of 27

A pressure gauge, drawn by hand.

MERIDIAN is one of 27 sites in the Formwork collection. Its signature is a readout you can watch work: a live analog gauge on a real <canvas>, stats that roll into place like an odometer, and a copper line that fills as you scroll. No chart library, no animation framework — just a couple hundred lines of vanilla JS.

The idea

Espresso is a measured thing: nine bars, ninety-three and a half degrees, eighteen grams in, thirty-six out. The brand promise is precision, so the interface had to feel like an instrument. Everything that carries a number is treated as a readout — it moves the way a dial or a counter moves, never the way a marketing headline fades in.

The stack

Signature technique: the readout

The gauge

The gauge runs one continuous extraction cycle: pressure ramps to nine bars on an ease-out, holds while the shot timer and yield count up, drops, rests, and loops. State is a pure function of a single clock t, so the drawing code never has to remember anything:

function stateAt(t) {
  let bar;
  if (t < RAMP)             bar = easeOut(t / RAMP) * MAXBAR;
  else if (t < RAMP + HOLD) bar = MAXBAR;
  else if (t < SHOT_END)    bar = MAXBAR * (1 - easeIn((t - RAMP - HOLD) / DROP));
  else                     bar = 0;
  // timer and yield are read off the same clock…
  return { bar, timer, yld };
}

The animation loop is delta-timed, so the cycle takes the same wall-clock time on a 60 Hz or 120 Hz display, and devicePixelRatio is capped at 2 so a 3× phone doesn't render a nine-megapixel dial. An IntersectionObserver stops the loop entirely once the hero scrolls away:

const dt = Math.min(0.05, (now - last) / 1000); last = now;
if (visible) t = (t + dt) % CYCLE;      // advance only while on screen
draw(stateAt(t).bar);

The odometer

Every stat is a row of clipped columns, each holding a 0–9 reel. To show a digit you slide its reel up by that many ems. rollTo only rebuilds the DOM when the shape of the value changes (say 27 s3:15); otherwise the existing reels animate straight from the old value to the new one, which is what makes the brew-card numbers spin when you switch methods:

function rollTo(el, str, animate) {
  const mask = str.replace(/[0-9]/g, '#');
  if (el._mask !== mask) buildOdometer(el, str);  // shape changed → rebuild reels
  let ci = 0;
  for (const ch of str)
    if (ch >= '0' && ch <= '9')
      cols[ci++].firstChild.style.transform = `translateY(-${+ch}em)`;
}

The interactive patterns

Four components carry the page. Each is a real, keyboard-accessible control — not a picture of one — and each is grounded in a pattern that ships in production software.

Segmented control

A genuine role="tablist" with a sliding thumb positioned by offsetLeft/ offsetWidth, roving tabindex, and arrow / Home / End keys. Grounded in the inline pricing pills of Webflow and the tabs-wired-to-a-panel pattern from 1Password — a filled active segment that swaps the content below it immediately.

Numbered stepper

Six <button> rows with aria-current, a sticky image panel, and a thin auto-advance bar that pauses on hover, focus, reduced-motion, or when scrolled offscreen. Grounded in the numbered walkthrough steppers of Sequence and the list-left / art-right layout of Mural.

Tier cards

Three flat machine cards separated by hairlines rather than boxes, one CTA each, with a single copper "MOST BUILT" flag on the middle card. Grounded in the dark, flat, hairline-divided product cards of Grok and the single-CTA tier ladder of Claude's pricing.

Details that matter

A dial, a reel of digits, and a line down the edge of the page — the whole site is one instrument reading itself out.