Ember & Oak is one of 25 sites designed and built by Formwork to show off web craft, motion, and taste. Here's exactly how the warmth was made — and how you can rebuild it.
Coffee is a tactile, sensory thing, so the brief was to make a page feel warm and physical rather than slick. Kraft-brown paper, cream ink, a single burnt-amber accent, and a characterful serif do most of the emotional work. The one moving thing that sells it is steam — real, drifting steam rising off a cup at the top of the page.
textPath ring spun with a pure-CSS @keyframes rotation.main branch.Each puff is a particle that rises, cools and spreads. It fades in and out over its life with a
sine envelope, and its horizontal drift widens with height — that's what reads
as wispy turbulence rather than a straight column. Puffs are drawn as soft radial gradients with
globalCompositeOperation = 'lighter' so they glow warmly against the dark kraft.
const t = p.life / p.max; // 0 → 1 over the puff's life
p.y -= p.vy * dt * (1.15 - t * 0.5); // rise, decelerating as it cools
const x = p.x + Math.sin(p.life*0.001*p.swaySpeed + p.phase) * p.sway * t;
const r = p.r0 + p.grow * t; // grow as it spreads
const a = Math.sin(Math.PI * t) * p.alpha;// fade in, then out
const g = ctx.createRadialGradient(x, p.y, 0, x, p.y, r);
g.addColorStop(0, `rgba(244,235,214,${a})`);
The emitter isn't a fixed point — it's measured from the live position of the cup SVG, so the steam always leaves the rim even as the parallax nudges everything around. The device-pixel-ratio is capped at 1.5 so it stays smooth on retina screens, and a hidden-tab check parks the loop when you're not looking.
.js class, and revealed by an IntersectionObserver, so a CDN hiccup never leaves a blank page.--fill custom property.prefers-reduced-motion the steam, spin and parallax all switch off and every reveal is forced visible.soft-light gives the whole surface a tactile, printed feel.Every site here is static, so hosting is three commands:
git init -b main && git add -A && git commit -m "ship"
gh repo create formwork-roastery --public --source=. --push
gh api --method POST /repos/OWNER/formwork-roastery/pages \
-f 'source[branch]=main' -f 'source[path]=/'
Use relative paths (./main.js, not /main.js) because
project Pages live under /repo-name/. Add an empty .nojekyll file so
every asset serves verbatim.
That's the whole recipe: one particle system, a little parallax, honest type, and static hosting. The other 24 sites each swap the central technique — shaders, kinetic type, 3D scenes — but the discipline is the same.