Motion grammar
UI motion follows four named durations and one default ease so transitions stay coherent. Bloom motion is documented separately in Surfaces / blooms.
Four durations
| Token | Value | Use |
|---|---|---|
--motion-fast | 120ms | hover, focus fade-in |
--motion-base | 220ms | button, input, pill — default |
--motion-slow | 320ms | sheet, modal, panel |
--motion-deliberate | 480ms | hero, marquee, dynamic-card stream-in |
A fifth duration, --motion-instant (0ms), exists only to express "snap to end" — it is the same value reduced-motion collapses everything else to.
Default easing
--motion-ease-standard: cubic-bezier(.2, .7, .2, 1);Use this curve unless you have a documented reason not to. The other named eases are listed below — each covers one specific directional case, and the set is closed.
Easings — full grammar
The canonical easings live in @matter/tokens as --motion-ease-*. Pick by the direction of the transition; the curve follows from the name.
| Token | Curve | When |
|---|---|---|
--motion-ease-standard | cubic-bezier(.2, .7, .2, 1) | default — symmetric in / out, used for hover, focus, pill, button, input |
--motion-ease-entrance | cubic-bezier(0, 0, .2, 1) | elements arriving on-screen — sheet open, toast slide-in, popover reveal |
--motion-ease-exit | cubic-bezier(.4, 0, 1, 1) | elements leaving — sheet close, toast dismiss, popover hide |
--motion-ease-emphasize | cubic-bezier(.4, 0, .2, 1) | continuous transitions that need a clear arc — tab indicator, segmented control |
--motion-ease-linear | linear | progress, spinners, marquee — any motion that must not accelerate |
--motion-ease-cubic | cubic-bezier(.65, 0, .35, 1) | symmetric ease-in-out for crossfades and tween-driven dynamic-card streams |
--motion-ease-soft-bounce | cubic-bezier(.34, 1.2, .64, 1) | reserved for affirmative micro-moments — success check, capture flash |
--motion-ease-snappy | cubic-bezier(.5, -.5, .25, 1.5) | overshoot + settle — drag-release on a Kanban card, pill toggle commit |
--motion-ease-anticipate | cubic-bezier(.4, -.4, .25, 1) | small pull-back before launch — used sparingly on confirm-style CTAs |
Soft-bounce, snappy, and anticipate are opt-in — the standard, entrance, exit, emphasize, linear, and cubic set covers every default flow.
Reduced motion
@media (prefers-reduced-motion: reduce) collapses all four durations to 0ms and stops bloom keyframes. Single source of truth — the rule lives once in @matter/tokens/css/motion-recipes and every consuming app inherits it.
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}The bloom orbs, the dynamic-card stream-in, the sheet overlay — all collapse to a static cross-fade under that media query.
Durations
Easings
Brand layer — durations, staggers, brand easings
The brand layer in @repo/brand extends the canonical motion grammar with longer durations for marketing-surface choreography, staggers for sequenced reveals, and two brand easings that pair with --duration-glow and the painterly bloom keyframes. These live in packages/brand/src/tokens/motion.ts and ship as their own CSS vars so the marketing-surface motion stays severable from app-surface motion.
Durations
| Token | Value | Use |
|---|---|---|
--duration-fast | 180ms | brand-surface hover and focus tinting |
--duration-short | 240ms | brand-surface input and pill transitions |
--duration-med | 420ms | section reveal — the rhythm marketing pages cross-fade on |
--duration-long | 520ms | hero choreography — title settles, eyebrow pulls in |
--duration-reveal | 720ms | brand-only "first paint" reveal on the landing canvas |
--duration-glow | 2500ms | painterly glow cycle on bloom art and signature backgrounds |
--duration-glow is a cycle, not a one-shot transition — it pairs with --motion-ease-cubic on infinite-iteration keyframes only.
Staggers
| Token | Value | Use |
|---|---|---|
--stagger-row | 80ms | per-row delay on Kanban / list stream-in |
--stagger-line | 70ms | per-line delay on hero title and section eyebrow reveal |
Use staggers as a multiplier on a sibling index — never as a duration. The companion duration is whichever the surface needs; the stagger only spaces out the start.
Brand easings
| Token | Curve | When |
|---|---|---|
--ease-out-soft | cubic-bezier(0.22, 1, 0.36, 1) | the brand-default deceleration — every --duration-med / --duration-long transition uses this |
--ease-in-out | cubic-bezier(0.4, 0, 0.2, 1) | symmetric brand transitions — used for --duration-glow painterly cycles and bloom drift |
--motion-ease-standard unless the transition is directional. Reach for the brand-layer duration / stagger tokens on marketing surfaces only.cubic-bezier() value. The canonical set covers every directional case; the brand set covers every choreography case.Authoring rules
Rules every contributor follows. Apply equally to designers in Figma, developers in CSS/JSX, and agents generating UI from prompts.
Voice & casing
Sentence case everywhere, two-word Title Case on buttons, no emoji except ✺ and › / ‹, no competitor comparisons. Tone is precise · confident · humble · future. Italics are banned.