← BACK

D&M-043025 / Logs · May 2, 2025

How this was built,
and what it teaches.

This is a record of the D&M microsite — not just how each part works, but why each choice was made and what the alternative would have looked like. Written May 2, 2025. Live examples throughout.

§0

Why this exists

A show that needed a site that felt like the show

D&M is a monthly show in SF — five-minute demos, no slides, designers and engineers in a room. When it needed a site, the obvious paths were wrong. A PDF felt dead. A standard Next.js page with a nav and a grid felt corporate.

The receipt metaphor came from a diner check someone always had to split: each demo is a line item, the host is the server, the date is the table. The cards on a dark surface are objects scattered on a table before a show starts — draggable because things on a table should move, with physics because physics is the difference between a toy and a thing. Every decision below traces back to one requirement: a surface you touch, not a document you navigate.

§1

The Canvas

A locked stage, not a scrolling document

A default browser page is a vertical document that scrolls. This site overrides that entirely. position: fixed; inset: 0; overflow: hidden on the root element turns the viewport into a locked stage — nothing scrolls, nothing flows. The entire surface is 100vw × 100vh and stays there.

Everything on that stage is position: absolute, moved by CSS translate3d(x, y, 0). React holds the (x, y) coordinates as state. The CSS transform turns those numbers into screen positions. That's the whole model: state in → pixels out, mediated by a transform property.

Document flow vs stage — toggle to see the difference

ticket
wonder
onlook
dialkit
sota
display: flex · flex-wrap · normal document flow

Document: flex pushes cards into rows. Overlap is impossible. Rotation breaks the flow. Drag would fight the layout engine.

translate3d vs translate: the 3D variant hints to the browser that this element should be composited on the GPU. In practice, modern browsers promote both — but translate3d is an explicit signal and has been the reliable choice for years. More importantly: transforms don't trigger layout. Changing left and toprecalculates the entire layout tree on every frame. Transforms don't. For 60fps drag, that difference is everything.

§2

Drag Physics

Pointer events, EWMA velocity, and momentum

Drag is built on pointer events — not mouse events, not touch events. Pointer events are the unified model: pointerdown, pointermove, pointerup work identically for a mouse, a finger, or a stylus. Each active pointer has a unique pointerId, which lets you track which specific finger or tool is dragging.

Why drag bypasses React

pointermove fires on every frame while dragging — up to 120 times per second on high-refresh displays. Calling setState on each event runs the full React reconciler: virtual DOM diff, fiber scheduling, commit phase, layout effects. The card stays smooth in isolation, but as the tree grows the frame budget shrinks and jank appears.

The fix is to skip React entirely during drag. Write the position directly to element.style.transform via a useRef to the DOM node. The browser's compositor picks up the transform change and moves the GPU layer — no JS heap allocation, no diffing, just a 4×4 matrix update. React gets the final position exactly once on pointerup. The two cards below make this visible: drag both and watch the counters.

setState path vs direct DOM path — drag both cards

setState

re-renders every frame

1renders

direct DOM

renders only on release

0renders

The tradeoff: during drag, React state is stale. Any code that reads position — collision detection, z-order updates — must read from the ref, not from state. The ref is the source of truth until pointerup commits it back into React.

EWMA velocity

Velocity is tracked with an EWMA — exponential weighted moving average. Each frame: velocity = velocity × 0.6 + frameDelta × 0.4. The 0.4 is the smoothing factor (α). Without smoothing, a single fast frame spikes the velocity and the card flies off the screen.

EWMA α — tune the smoothing factor

α — smoothing factor0.40 ← site
rawsmoothed
balanced

velocity = velocity × 0.60 + frameDelta × 0.40

On release, the landing position is computed as land = position + velocity × 9. The 9 is the momentum multiplier — tuned by feel. The CSS spring transition then animates the element from its current DOM position to the computed landing point.

Momentum carry — tune the multiplier

momentum multiplier× 9 ← site
drag + throw
throw to reveal ghost outlines
weighted but controlled

Live — drag the card, watch the debug overlay

drag

DEBUG

x 215   y 67

vx 0.0   vy 0.0

tilt 0.0°

idle

Blue arrow = velocity vector (direction × magnitude). Momentum carry = final position + velocity × 9.

The blue arrow is the velocity vector: direction matches movement direction, length scales with speed. The debug panel shows phase transitions — idle → dragging → springing → idle.

Two-layer transform

The site uses two nested divs for every draggable item. The outer div handles translate3d (position). The inner div handles rotate (base angle + live tilt). Separating them means each can transition independently — position springs to a landing point while tilt snaps to zero — without compound transform math.

The tilt is the horizontal velocity component mapped to a rotation angle: tilt = clamp(velX × 0.9, -14°, 14°). Fast rightward movement tilts the card clockwise. It resets to 0 on release without interfering with the position spring.

Interactive — slide to see the two layers

outer · translate3d
inner · rotate
card
translate3d X(32px, 24px, 0)
rotate-8°

Separating layers means tilt resets independently of position — no compound transform math on release.

§3

Visual Depth

Shadows, grain, and blend modes

Flat colored rectangles on a flat background look like UI. To make objects look like things, you need depth cues: multi-layer shadows, surface texture, and light interaction.

Box shadows are composited. The receipt uses two: a tight, dark shadow directly below the card simulates a contact shadow close to the surface; a large, soft shadow simulates light scattering at distance. Together: 0 4px 0 rgba(0,0,0,0.55), 0 10px 36px rgba(0,0,0,0.5).

The grain is an SVG filter with no image file. The feTurbulence filter generates fractal noise mathematically — the same Perlin-noise algorithm used in game engines and shader programs. Encoded as a data URI and applied as a background-image, it costs zero network requests. The baseFrequency attribute controls coarseness.

SVG feTurbulence grain — four configurations

coarse

freq 0.45 · normal

default

freq 0.72 · normal

fine

freq 1.20 · normal

paper ×

freq 0.90 · multiply

Interactive grain tuner — adjust frequency and opacity

baseFrequency0.72
opacity0.18

Mid-range — reads as paper grain.

The paper surface uses mix-blend-mode: multiply. Multiply compositing works like ink on paper: dark areas darken further, light areas pass through. This makes the grain feel embedded in the paper surface rather than layered on top.

mix-blend-mode: difference is used for the hover labels below each card. Difference inverts the color channel against the background. White over dark becomes white; white over light becomes dark (nearly black). The same element reads legibly on any surface without being told what the background is.

mix-blend-mode: difference — same element on different backgrounds

label

dark canvas

label

light card

label

colored surface

label

split

Same element, same color. mix-blend-mode: difference inverts the color against whatever is underneath — the text is always readable without knowing the background.

§4

The Receipt

A paper artifact as UI metaphor

The central card is a diner check — each demo is a line item, the host is the server, the date is the table. The metaphor drives every visual decision on this component. When the metaphor is strong enough, the design decisions make themselves.

Three typefaces in one component: ui-monospace (system monospace) for printed labels and numbers — it reads as machine-typed; Caveat (Google Fonts, variable: --font-hand) for the title and demo names — it reads as pen on paper; no sans-serif anywhere. The component speaks in one voice.

Typeface system — compare the options

Designers & Machines

5-min demos · no slides · san francisco

01wonder
02hand of you
03onlook
04dialkit for ios
05sota zine

Title is pen-on-paper. Items are machine-stamped. One typeface for feeling, one for reading. The diner check speaks.

The title uses Caveat (handwriting variable font) — loaded via --font-hand.

Item names and numbers use ui-monospace — the system monospace, no extra load.

The receipt — static

Designers & Machines

5-min demos · no slides · san francisco

01wonder@aibek_design
02hand of you@buburdin
03onlook@onlookdev
04dialkit for ios@harshitbeni
05sota zine@eve_bouff

GUEST RECEIPT · D&M-043025

The dashed border between the stub and the body is border-top: 3px dashed — the tear-here perforations of a real receipt. The ruled lines between items are border-bottom: 1px solid on each row. The grain on the receipt uses baseFrequency='0.9' — higher than the canvas grain — giving a tighter paper texture.

§5

Animation

Spring easing, View Transitions, and staggered entrances

Most CSS easing curves trace a path from 0 to 1 and stop there. The interesting ones overshoot. A spring easing — cubic-bezier(0.16, 1, 0.3, 1) — hits fast and slows into the target. The overshoot variant — cubic-bezier(0.34, 1.56, 0.64, 1) — blows past before settling back. At 0.7s, you can feel the gap.

Easing comparison — same distance, same duration

linear
ease-out
spring
overshoot

View Transitions is a browser API that morphs between two DOM states. You call document.startViewTransition(callback). The browser takes a screenshot of the current state, runs your callback (which changes the DOM), takes a second screenshot of the new state, then cross-fades between them while also animating any elements that shared a view-transition-name.

The receipt uses this for expand/collapse: the small card on the canvas and the full-screen overlay both carry viewTransitionName='receipt'. The browser automatically generates a smooth FLIP animation between the two positions and sizes without any JavaScript position math.

flushSync is the subtle requirement: by default, React batches state updates and applies them asynchronously. Inside a View Transition callback, the browser captures the DOM immediately after the callback returns — before React has applied the update. flushSync forces React to commit synchronously so the second snapshot captures the new state.

The receipt expand/collapse animation was inspired by Cambio by Raphael Salaja — a shared animation component built on Base UI and Motion that demonstrates the same expand-from-card pattern using the View Transitions API.

The staggered entrance uses a double requestAnimationFrame idiom: setTimeout → rAF → rAF → setState(visible). One rAF isn't enough — the element might not be painted yet. Two rAFs guarantee the browser has run a layout and paint pass before the opacity/blur transition begins, so there's always a visible starting state to transition from.

§6

Color

oklch and the perceptual color model

The site uses oklch — a color space defined by three axes: L (lightness, 0–1), C (chroma, 0–0.4+), and H (hue angle, 0–360). Unlike hex or hsl, oklch is perceptually uniform: equal steps in L produce equal changes in perceived brightness.

The practical benefit: you can change hue without accidentally shifting brightness. In hsl, rotating from blue (220°) to orange (40°) at the same saturation and lightness looks darker and duller because the human eye doesn't perceive hsl values uniformly. In oklch, it stays the same.

The site palette was chosen to feel warm on a dark ground — background at oklch(0.14 0.008 260) is a very dark blue-grey, paper at oklch(0.92 0.006 80) is a warm near-white. The hue shift between them (260 → 80) creates the tension between the dark stage and the warm paper cards.

Site palette

background

L 14% · cool tint

surface

L 22%

text

L 88% · warm

body

L 74%

paper

L 92% · warm

ember accent

C 0.20 · orange

In oklch, L (lightness) is perceptually uniform — doubling L actually looks twice as bright. In hex or hsl, it doesn't. This means you can shift hue (H) without accidentally brightening or darkening the color.

Interactive oklch explorer — tune L, C, H

oklch(0.65 0.15 220)

L 65% · C 0.15 · H 220°

L — lightness65%
C — chroma0.15
H — hue angle220°

Same L=65% — oklch vs hsl across hues

orange

oklch · hsl

lime

oklch · hsl

teal

oklch · hsl

blue

oklch · hsl

purple

oklch · hsl

red

oklch · hsl

At the same L, oklch colors look equally bright across hues. In hsl, yellow and green appear lighter than blue and red at identical L% — the perceptual unevenness oklch fixes.

§7

Vocabulary

Terms this project adds to your toolkit

Every project deposits language. These are the terms you can now name, reach for, and explain after building this.

pointer eventsinput model
The unified input API covering mouse, touch, and stylus with a single event surface. Each contact has a pointerId — essential for multi-touch.
EWMAsignal processing
Exponential Weighted Moving Average. A smoothing filter: new_value = old × (1−α) + sample × α. Controls how quickly the average responds to change.
momentum carrymotion design
On release, adding final velocity × multiplier to compute a landing position. The spring then animates to it. Creates the feeling of physical weight.
FLIP animationanimation technique
First, Last, Invert, Play. Record the start position, record the end position, invert the transform to start, then play it forward. View Transitions implements this for you.
spring easinganimation
A cubic-bezier tuned to approach its target asymptotically rather than at constant velocity. Fast start, long settling tail. Feels physical.
translate3dCSS
A 3D CSS transform that signals GPU compositing. The element gets its own compositor layer, which means transforms and opacity animate without triggering layout.
view-transition-nameCSS / Web API
A CSS property that enrolls an element in View Transitions. Paired elements across before/after snapshots are morphed automatically by the browser.
flushSyncReact
Forces React to commit a state update synchronously. Required inside View Transition callbacks where timing is controlled by the browser, not React's scheduler.
feTurbulenceSVG filters
An SVG filter primitive that generates procedural noise. fractalNoise mode produces grain; turbulence mode produces fluid distortion. No image file needed.
mix-blend-modeCSS compositing
Controls how an element composites against its background. multiply darkens; difference inverts; screen lightens. Removes the need to know the background color.
oklchcolor
A perceptually uniform color space: L (lightness), C (chroma), H (hue). Uniform means equal numeric steps produce equal perceived changes — unlike hsl.
drag thresholdinteraction design
A minimum distance (5px here) before a pointer-down is classified as a drag rather than a click. Without it, no link inside a draggable element would ever fire.
direct DOM mutationperformance
Writing to element.style directly, bypassing React's reconciler. The escape hatch for animation: React would re-render the whole tree on every pointermove frame.
data URIweb
Inlining a file's content as a base64 or URL-encoded string in src or background-image. No network request. Used here for the grain SVG.

D&M-043025  ·  designers-machines.floguo.com  ·  last updated May 2, 2025