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.
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.
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
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.
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
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
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
Live — drag the card, watch the debug overlay
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
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
Interactive grain tuner — adjust frequency and opacity
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
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
The receipt — static
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.
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
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.
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
Interactive oklch explorer — tune L, C, H
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.
D&M-043025 · designers-machines.floguo.com · last updated May 2, 2025