Skip to content
ArcadiaDesign

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.

Press
Enter
Leave
Fold
Scene
Swap
Status mark
Reorder
Glide
Focus
Character
Jump
PatternMovesTime, curve and travel
PressA control sets down at once under the pointer and springs back when let go.
ColourEvery hover, focus ink and colour change. Nothing lifts.
Row fillA row's hover fill comes in at once and leaves on the colour beat; the current row's pill changes in one frame.
EnterContent arriving: a stream row, a message, an event, a card.
LeaveContent going: it fades where it stands.
FoldA disclosure opening and closing on Unfold, its content fading in and out.
Chevronchevron-right turned by --icon-turn as its fold opens.
SceneA pane, a drawer, the sidebar's width, a rolling number.
SwapA page of content replacing another, out of a short blur.
Status markA dot or a badge popping in, or a row's dot turning unread, attention or error.
ReorderA row gliding to its new place in a list (useFlip).
GlideThe shared highlight moving between a list's rows, and appearing.
FocusA field's ink frame and halo.
OverlayDialogs, sheets, menus, popovers and tooltips, on the overlay scale below.
CharacterAn agent popping in, and its hop when a turn ends.
JumpThe row a search or a link landed on: its wash or ring, held, then fading.

Overlays

One scale off a dialog's open, 180ms.

SurfaceOpensLeavesCurveMoves
A dialog and its backdropFades in while it rises 8px; fades out where it stands.
A sheet and its scrimSlides in from its edge and back out to it while the scrim fades.
A toastRises 6px into the stack; fades out.
A menu, a popover, a hover cardSlides 4px toward its anchor; fades out where it stands.
A traySlides 4px out from the edge it docks to; fades out where it stands.
A tooltipThe 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.

Critically damped: it arrives without overshoot. A reordered row glides to its place on it.
ζ 0.84, ω 21.5: half-way in 72ms, 90% in 147ms, a 0.8% overshoot. Indicators that move ride it.
ζ 0.75: a 2.8% overshoot, felt rather than seen. The switch knob 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.

The period. A beam crosses for 70%, then 30% dark.
Working
CrossingDark
Half the watch: working text crosses, then rests.
BeamRestBeamRest
Half the watch: four 400ms beats.
00.8s1.6s2.4s3.2s
The playhead is the clock itself, linear. The beams ease across on --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.

TokenSampleUseValue
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 transitionend still 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.

Thinking
TSX

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%.