Animation conventions, entrance patterns, and working cues. Sources: kit theme/index.css animation vocabulary, speedway app.css keyframes, component-level evidence in the registers under docs/evidence/20260807-0230-speedway-design-language/ in this repo.
Audience: an agent (or person) adding or reviewing animation.
§A Principles and mental models#
1. Motion means "this just changed."
fadeInUp (0.3s) is the house convention for ANY value re-entering: a new toast, an updated pagination count, a changed cell. stagger-fadeInUp cascades children about 60ms apart, capped at the 6th child (both keyframes defined with dotPulse at kit theme/index.css:83-111). Components expose noAnimate to opt out. Motion is a change signal, not decoration.
2. Placeholders do not animate in. A skeleton "should be there, not arrive" (Card ruling). Skeleton pulse is the only skeleton motion, staggered with the bounded fractional-modulus offset so lists neither lockstep nor lag.
3. Working cues are dots, not extra spinners.
An in-flight state pulses its status dot (dotPulse on StagePill for ingesting/running, live at speedway JobsTable.tsx:156,167; the expanding-ring pulse on StatusDot). Where a pulsing dot already says "working", a second spinner is banned ("the stage badge's pulsing dot is the working cue, no second spinner"). Liveness is separate from status: the run heartbeat dot pulses green under 20s since last checkpoint, flat amber when stale, layered beside (not inside) the status badge.
4. Calm ambient cues down.
The has-suggestion dot slows animate-pulse to 2.8s explicitly (speedway review.tsx:989, [animation-duration:2.8s]) so it reads as ambient, not alert. Default animation speeds are for events. Ambient state gets slowed timing.
5. Feedback delays prevent flicker.
Nav sweep bar waits 90ms (speedway root.tsx:207-241). The clicked link's spinner waits 120ms (WorkspaceNav.tsx:104-131). Instant transitions never flash feedback. The sweep trickles to 88% and snaps on completion.
6. Entrances are shaped to their content.
rise (fade + translateY) for list rows and toasts; bar-grow (scaleX, 0.6s ease-out) with per-row 35ms delays for ranked share bars, a cascading reveal; card-arrive is a 1.6s :target highlight ring for arrival-by-anchor (these keyframes at speedway app.css:1592-1646). IconCycle cross-fades three glyphs (700ms opacity, 2200ms interval) with all icons mounted "so the fade never shifts layout."
7. Transitions are sparse and mostly kit-internal.
Page code rarely writes transition-*. Observed exceptions are deliberate: the Fix caret rotate, a scroll-cue fade (gradient curtain plus chevron driven by ResizeObserver, only in the bounded modal scroll region where fields were missed below the fold), and the activity feed's hover transition-colors. Dropdown exit animations live on the inner panel div (the two-div rule, doc 10). Admin surfaces carry little motion. That austerity is acceptable there.
8. Respect reduced motion.
prefers-reduced-motion: reduce disables skeleton and toast animation. Any new keyframe must join that block. In the kit that block lives in the theme stylesheet (packages/ui/theme/index.css:144): entrances snap to their end frame instead of vanishing, and the landing hero's drift (heroDrift, the first keyframe added under this rule) stops. A keyframe outside the block is a defect.
9. A segment enters as one; its children follow.
Added 2026-08-17 from the owner's second look at the playground (doc 59 G2): a page where the title fades up but the breadcrumbs, the See also row and the lead cards snap in bare reads as half-built. The rule is one entrance on the segment (a page's content column, a title block, a card group), so everything inside arrives together, and the pieces that earn their own motion (a card's stagger-fadeInUp, a title's animate-fadeInUp, the page title's icon disc a beat later) run inside that entrance rather than instead of it. The kit names the segment class animate-segmentIn (packages/ui/theme/index.css), the playground puts it on PageFrame's column (apps/playground/src/app/page-frame.tsx), and every kit entrance is stilled together under prefers-reduced-motion (§A8). Never leave a sibling of an animated element to appear with no entrance at all; either it rides the segment or it has its own.
§B Presets#
| Situation | Preset |
|---|---|
| Value changed | fadeInUp; stagger for sibling groups |
| Active process | dotPulse / StatusDot pulse |
| Ambient hint | animate-pulse slowed to ~2.8s |
| Navigation pending | 90ms-delayed sweep + 120ms-delayed link spinner |
| Ranked list reveal | bar-grow + 35ms per-row delay |
| Arrive by anchor | card-arrive :target ring |
| Busy icon slot | IconCycle cross-fade |
| Progress fill | width transition 0.45s (mock) or static fill; never bounce |
§C Use cases through Speedway#
- StagePill pulse distinguishes actively-happening from static done/error/queued. RunLiveness heartbeat sits beside JobStateBadge.
- ScrollCue only in the create-job modal's bounded region. The full page opts out (whole page already scrolls).
- GlobalsCard share bars cascade. Toasts rise. Skeleton pulse everywhere content streams.
- ProgressCell renders a static fill (no transition) on re-render. The mock's animated 0.45s width transition is the aspirational preset for new work.