Motion
Durations, easings and two springs as tokens, one clock for every live loop, and one attribute for reduced motion.
Calm is the point. Motion is named by pattern, not by the surface that first used it: one Enter, one Leave, one Fold. Hovers change colour only, over one 150ms heartbeat on --ease-standard; nothing lifts, and a pressed control sets down 2% and springs back. --ease-enter is the one deceleration; a pane's Scene and an overlay keep curves of their own. Overlays open on one scale: a dialog rises 8px and fades in over 180ms, floating surfaces slide 4px toward their anchor in 120ms, a tooltip in 90ms, and none zooms. Springs move only things that actually move: a segmented thumb, a tab underline, a reordered row. Scrolling stays native, and nothing pops in on scroll; only a scroll the app starts (a jump to the latest message, PageDown) rides a spring, described on Scroll area. Only live changes animate: loading a thread, switching threads and collapsing in bulk are static (data-instant). Every live loop divides one clock.
A list that reorders glides: useFlip moves each row from where it was to where it is over --duration-reorder on --ease-settle, with translate alone, and snaps when more than eight rows move at once. No animation gates input, and none moves the layout: enters, exits, pops, presses and reorders are transforms and opacity on boxes already in their final place.
A few things change a box's size, each on the user's own action and each replacing a snap. Unfold is the one fold: every disclosure opens on it, from no height to its content's, with a chevron-right turning beside it. useHeightSettle lets a box whose content changed height run from the old height to the new one over --duration-fold on --ease-enter, the overflow clipped: a card that collapses, a reply taking a shorter row's place, a one-line composer becoming a card. It snaps when the width changed too, while content is still streaming, when something inside already eases its own height, and out of view. A side pane slides in at its final width while its column opens, so its content never re-wraps on the way. ViewStack moves a pane between a list and what it opened.
Patterns
Every motion in the kit is one of these. Each dot crosses on its pattern's time and curve; click a track to run it again, or replay them all to compare.
| Pattern | Moves | Time, curve and travel |
|---|---|---|
| Press | A control sets down at once under the pointer and springs back when let go. | |
| Colour | Every hover, focus ink and colour change. Nothing lifts. | |
| Row fill | A row's hover fill comes in at once and leaves on the colour beat; the current row's pill changes in one frame. | |
| Enter | Content arriving: a stream row, a message, an event, a card. | |
| Leave | Content going: it fades where it stands. | |
| Fold | A disclosure opening and closing on Unfold, its content fading in and out. | |
| Chevron | chevron-right turned by --icon-turn as its fold opens. | |
| Scene | A pane, a drawer, the sidebar's width, a rolling number. | |
| Swap | A page of content replacing another, out of a short blur. | |
| Status mark | A dot or a badge popping in, or a row's dot turning unread, attention or error. | |
| Reorder | A row gliding to its new place in a list (useFlip). | |
| Glide | The shared highlight moving between a list's rows, and appearing. | |
| Focus | A field's ink frame and halo. | |
| Overlay | Dialogs, sheets, menus, popovers and tooltips, on the overlay scale below. | |
| Character | An agent popping in, and its hop when a turn ends. | |
| Jump | The row a search or a link landed on: its wash or ring, held, then fading. |
Overlays
One scale off a dialog's open, 180ms.
| Surface | Opens | Leaves | Curve | Moves |
|---|---|---|---|---|
| A dialog and its backdrop | Fades in while it rises 8px; fades out where it stands. | |||
| A sheet and its scrim | Slides in from its edge and back out to it while the scrim fades. | |||
| A toast | Rises 6px into the stack; fades out. | |||
| A menu, a popover, a hover card | Slides 4px toward its anchor; fades out where it stands. | |||
| A tray | Slides 4px out from the edge it docks to; fades out where it stands. | |||
| A tooltip | The same slide, on the last step. |
An overlay takes its times from this scale and from no other token, and the kit's lint holds it. Its first painted frame is the first of its enter, and its clock starts on that frame.
Set down
A dialog starts --dialog-rise (8px) low and fades in as it rises, both over --duration-dialog on --ease-in-out, while its backdrop blurs the page on the same times.
It leaves with a --duration-dialog-exit fade where it stands.
The less a surface covers, the sooner it is there: what floats from an anchor takes two thirds of it, a tooltip half, and each leaves in three quarters of its own enter.
Springs
Three springs, sampled into linear() so CSS can run them: one for arriving without overshoot, one for things that move, one for coming to rest.
Release
A pressed button sets down to --press-scale at once and comes back on --ease-release over --duration-release, as the switch knob comes to rest: a hair past its stop.
The curves are drawn from the tokens' own stops.
One clock
--loop-watch (3.2s) is the period and every live loop divides it.
--ease-sweep, slow off the edge and quick through the middle, so they run ahead of it; the pine lights each frame's stroke as the playhead enters that frame.One-shot entrances are never pinned, so a pop that happens to be running keeps its own start.
The shimmer and the pine move only transforms and opacity on boxes of their own, which the compositor runs without repainting the page. They, the spinner and the characters pause while out of view, while the window is hidden or unfocused, and after a minute with no input; back in view the shimmer, the pine and the spinner rejoin the clock.
An agent's character rides the same clock: it rests still, sways over one and a half periods while it thinks and bobs over a quarter while it writes. Each character keeps its own phase, so a list of agents never moves in step.
Other loops
Busy and placeholder loops are not live signals, and keep their own periods.
| Token | Sample | Use | Value |
|---|---|---|---|
| Spinner: busy, not live. | |||
| Skeleton: a placeholder breathing. |
syncAnimations pins each loop to the document timeline, so a running agent's signal, its running glyph and its working text restart together however late they mounted.
Reduced motion
One mechanism: <html data-reduce-motion="on">, set before first paint by the boot script and kept in sync by the hooks.
The contract
- Distances and scales collapse: the popup slide, the dialog rise, the slab's drop, tray, enter and swap offsets, the swap blur, the glyph morph, the press and every pop all go to rest, a fold snaps open and shut, and the shared highlight and a reordered row jump instead of gliding. Enter becomes a 150ms fade. An overlay still fades in and out where it stands, on its own step of the scale, and fades of 150ms or less stay.
- Loops stop. A still glyph and its text carry the meaning: the spinner holds its arc, the pine shows its whole mark, and a running live signal holds a steady lamp.
- Size and position transitions drop to 0.01ms rather than none, so
transitionendstill fires (the kit's popups complete at once when nothing runs). - JavaScript motion checks
prefersReducedMotion(), which reads the same attribute.
Try it
This switch is the site's own preference (it is also in the top bar's options menu). Replay the tracks above with it on.
The springs are sampled from real spring physics into linear() stops: settle is critically damped (ζ 1) over 320ms, morph is ζ 0.84 at ω 21.5 over 410ms (0.8% over), and release is ζ 0.75 over 280ms, which overshoots by 2.8%.