Foundations
Motion
The surfaces of this system are deliberately quiet, which puts the whole expressive budget on what happens when you touch them. That budget is not unlimited: motion earns its place by explaining something, and everything else is decoration that has to survive being switched off.
What motion is for
Explain a change
A sheet rises from the edge it will return to. A tab indicator travels rather than teleports. The motion says where something came from — which is information the static frames cannot carry.
Confirm a touch
Ripple, press squish, haptic tick. The interface answers within one frame of contact, before the actual work has finished, so the tap never feels dropped.
Direct attention
An entrance reveal or a counting number pulls the eye to what just arrived. Used sparingly this is useful; used on everything it is noise with a preference for the last thing rendered.
Press: the answer within one frame
Every interactive surface here responds before anything else happens: a ripple from the exact contact point, a squish under the finger, a haptic tick on commit. Press and hold one of these — the ink stays at full strength and only fades once the grow has landed, so even a 40ms tap plays the whole gesture. Drag off it and the press cancels, because a touch that turns into a scroll was never a press.
Presses so far: 0. On a phone the third tile also fires a haptic — a no-op where the Vibration API is missing, so nothing needs feature-detecting at the call site.
Entrance
Reveal animates content in when it scrolls into view, once, with
an optional stagger. The important property is the failure mode: if the
observer never fires — no JS, no IntersectionObserver, reduced
motion — the content is visible, not stuck at zero opacity. An
entrance animation must never be able to hide the page.
How it runs
Three steps, one afternoon
Editorial plan
Twelve posts for week 34.
Production
Captions, carousels and cuts.
Autopilot
Approve once, or keep every post in review.
Numbers
Counter decelerates into the final digits, which is the part the
reader cares about — a linear count-up reads like a loading spinner. Press
"new numbers": each value animates from where it currently is, not from zero,
and the figures are tabular so the number does not jitter sideways as digits
change.
Impressions
0.0k
+12.4%Engagement
0.00%
+0.8ptCredits
0
−320A navigable space
Spatial is the largest thing in the system: a 3D stage you can
look around. Drag to rotate, pinch or ⌘-scroll to zoom, double-click to reset;
with a keyboard, arrows rotate and +/- zoom. It is
CSS 3D rather than WebGL, so the panels are real DOM — selectable text,
focusable links, no canvas — which is the right trade until you need
thousands of objects or real lighting.
Services
Social media designs & feed creation
//2026 · drag to look
Different
Week 34 · 12 plates
- Transitions switch off while a pointer is down, so the stage tracks the finger exactly and re-eases only for the release and for programmatic moves.
- Release carries velocity and decays it. Without inertia every gesture ends in a dead stop, which reads as the page catching you.
- Plain wheel scrolling is deliberately not captured — the page still scrolls over the scene, and a trackpad pinch already arrives as ctrl+wheel, so the natural zoom gesture works anyway.
- Vertical rotation is clamped. A stage that can go upside down is a stage a user can get lost in.
Choosing duration and curve
/* Duration follows distance, not importance.
A 6px state change and a full-screen sheet are not the
same event, and giving them the same 300ms makes one
feel sluggish and the other abrupt. */
--an-duration-short-2 /* 100ms — colour, opacity, state layers */
--an-duration-short-4 /* 200ms — small moves, hover, press */
--an-duration-medium-2 /* 300ms — menus, popovers, tab indicator */
--an-duration-medium-4 /* 400ms — dialogs, sheets, panel travel */
--an-duration-long-2 /* 500ms — full-screen or first-run motion *//* Entering decelerates, leaving accelerates: things
arrive gently and get out of the way quickly. */
--an-ease-emphasized-decelerate /* enter: menus, dialogs, sheets */
--an-ease-emphasized-accelerate /* exit: dismissals */
--an-ease-emphasized /* travel between two positions */
--an-ease-standard /* colour, opacity, small states */
--an-ease-spring /* toggles and thumbs only */Both scales are mirrored in motion/tokens.ts, so a Svelte
transition and a CSS transition run the identical curve — see them animated on
the design tokens page.
Four ways motion goes wrong
Motion that carries meaning alone
It disappears under `prefers-reduced-motion`, and roughly one in three people ship that setting on. If the only way to know a save succeeded is that something moved, some users never know.
Easing during direct manipulation
A sheet or a 3D stage that eases while a finger is down lags behind the finger. Transitions belong to the release, not the drag.
Distance that lies
A 60px entrance says "this flew in from somewhere". It did not — the viewport moved. 8–16px is the whole budget.
Animation on every render
A counter that restarts from zero whenever a metric refreshes, a list that re-reveals on every scroll. Motion should mark a change, not a repaint.
Reduced motion is not a downgrade
Everything on this page has a defined behaviour when the OS asks for less motion: transitions collapse, ripples and idle drift stop, haptics are suppressed, reveals render visible, counters render their final value, and the 3D stage stays fully navigable — because direct manipulation is the user's own motion, not ours. Turn the setting on and the product still works; that is the test.