Motion tokens
The generated reference for every duration, easing and spring in the system, with each curve plotted from the parameters it was derived from.
Overview
This is the complete motion vocabulary: every duration step, every easing, every
spring, and for each one what it controls and which components consume it. It is
generated from tokens/motion.json by scripts/build-tokens.mts and it is the
only place motion values are published. Nothing on this page is typed by a
person, and pnpm check:generated fails the build if anyone tries.
Read it as a decision aid rather than a list. The third column tells you what each token is Used by, and that is what turns an undifferentiated list of durations into "here is the one you want". It is also the reason the table is generated: a hand-maintained list of consumers is wrong within a week.
How it works
tokens/motion.json holds three kinds of entry.
Durations are authored directly, in milliseconds, as an ordinal scale from
instant to deliberate. The names describe the perceptual character, not the
number, so that changing a value does not require renaming a token.
Easings are either a plain cubic Bézier, authored directly, or a spring.
Springs are authored as stiffness, damping and mass, and the generator solves
them numerically and emits a CSS linear() easing with a computed settling
duration. The whole derivation, including the source of springToLinear, is on
Springs as tokens. This is why a spring token has a
duration attached to it that nobody chose: it falls out of the physics.
Six of the thirteen entries also carry a reduced variant, which the generator
emits into @media (prefers-reduced-motion: reduce). The table below does not
have a column for it yet: scripts/build-tokens.mts records each one as
reducedMotionValue in lib/generated/tokens.ts, and no view publishes it beside
the full value so far. Reduced motion holds the per-token
contract until one does.
The generated custom properties land in app/tokens.generated.css, which is
imported by both the docs chrome and the product theme, so the curve you see in a
preview on this site is the same custom property a consuming product gets.
Using it
Find the job, then the token. The five jobs are enter, exit, feedback, navigation and loading, and Using motion maps each of them onto a step of this scale. Come here to confirm the value and to see what else uses it; go there to decide.
Check the damping ratio before you animate a number. Any easing whose damping ratio is below 1 overshoots, and an overshooting curve may not be applied to a measurement, a range position, a status or a count. The generated table publishes the ratio for exactly this reason.
Copy the token name, never the value. A linear() string pasted into a
component is unreadable, unthemeable and immediately stale. The whole point of
generating them is that nobody has to look at one.
If the token you want is not here, propose it. Adding a curve is a token change with a migration obligation, described on Contributing tokens; it is not something to work around locally.
Tokens
| Token | What it controls | Used by |
|---|---|---|
| --opsin-ease-spring-snaplinear(0, 0.0715, 0.2271, 0.4053, 0.5722, 0.7119, 0.8198, 0.8978, 0.9505, 0.9836, 1.0025, 1.0118, 1.015, 1.0147, 1.0126, 1.01, 1.0073, 1.0051, 1.0033, 1.0019, 1) | Direct manipulation only: a switch the reader just flipped, a segmented control, a pressed button settling. It overshoots by 1.5%. That is enough to feel physical but not enough to look playful. The 1.5% is the largest overshoot in the system but not the only one: `spring-settle` overshoots by 0.88%. Only `spring-calm` and `spring-sheet` reach their target without passing it. | no component |
| --opsin-duration-spring-snap283ms | Settle time for the spring-snap spring, measured from its own parameters. | no component |
| --opsin-ease-spring-settlelinear(0, 0.0742, 0.2328, 0.4113, 0.5758, 0.7116, 0.8157, 0.8905, 0.9412, 0.9736, 0.9928, 1.003, 1.0076, 1.0088, 1.0082, 1.0068, 1.0053, 1.0038, 1.0026, 1.0017, 1) | The workhorse for chrome: popovers, tooltips, menus, chips appearing and disappearing. | no component |
| --opsin-duration-spring-settle382ms | Settle time for the spring-settle spring, measured from its own parameters. | no component |
| --opsin-ease-spring-calmlinear(0, 0.0829, 0.2457, 0.4157, 0.5642, 0.6832, 0.774, 0.841, 0.8893, 0.9236, 0.9476, 0.9643, 0.9757, 0.9836, 0.9889, 0.9926, 0.995, 0.9967, 0.9978, 0.9985, 1) | A health value that changes while it is already on screen: a bar re-filling from one reading to the next, a dial travelling between two values the reader has already been shown. Never a first paint and never a first reveal. A value arrives at its final figure, with no count-up, no dial sweep and no line drawing itself in (health/motion-in-health-ui rule 2). Slightly overdamped (zeta just over 1) so it never overshoots and never bounces. | no component |
| --opsin-duration-spring-calm550ms | Settle time for the spring-calm spring, measured from its own parameters. | no component |
| --opsin-ease-spring-sheetlinear(0, 0.0881, 0.2576, 0.431, 0.5798, 0.6971, 0.7854, 0.8497, 0.8958, 0.9282, 0.9508, 0.9664, 0.9771, 0.9845, 0.9895, 0.9929, 0.9952, 0.9967, 0.9978, 0.9985, 1) | Large surfaces travelling a long distance: sheets, dialogs, full-screen pushes. Overdamped, because a sheet that bounces at the top of its travel reads as a dropped object. | dialog, sheet |
| --opsin-duration-spring-sheet483ms | Settle time for the spring-sheet spring, measured from its own parameters. | dialog, sheet |
| --opsin-ease-standardcubic-bezier(0.2, 0, 0, 1) | Non-spring transitions where a spring would be overkill: colour, opacity, border. Fast out, slow in. | accordion, body-map, button, checkbox, combobox, consent-sheet, dialog, menu, number-field, popover, progress, radio-group, scale-input, scroll-area, segmented-control, select, sheet, skeleton, slider, switch, symptom-picker, tab-bar, tabs, textarea, toast, tooltip |
| --opsin-ease-entercubic-bezier(0.05, 0.7, 0.1, 1) | Something arriving from off-screen or from nothing. Decelerating, because an arrival should feel like it is coming to rest. | no component |
| --opsin-ease-exitcubic-bezier(0.3, 0, 0.8, 0.15) | Something leaving. Accelerating and shorter than its enter, because a reader does not need to watch a dismissal finish. | dialog |
| --opsin-duration-instant80ms | State change with no travel: hover tint, focus ring, checkbox tick. | no component |
| --opsin-duration-fast140ms | Small elements moving a small distance. | accordion, body-map, button, checkbox, combobox, consent-sheet, dialog, menu, number-field, popover, progress, radio-group, scale-input, scroll-area, segmented-control, select, sheet, skeleton, slider, switch, symptom-picker, tab-bar, tabs, textarea, toast, tooltip |
| --opsin-duration-base220ms | The default for chrome that is not spring-driven. | dialog, sheet |
| --opsin-duration-slow360ms | Layout change: a list reflowing, a card expanding. | no component |
| --opsin-duration-deliberate560ms | The ceiling on a transition a reader is waiting on, such as a first-run reveal or a consent sheet, where the point is that the reader notices. No such transition may exceed it. A looping placeholder period, like the skeleton shimmer, is bounded instead by its iteration count, so a single sweep of it may run longer. | no component |
| --opsin-duration-shimmer1600ms | One sweep of a loading placeholder's sheen. This is a period, the time for the sheen to make one pass across the bar, and not a travel time. At 1600ms the movement reads as calm rather than as urgency, which the first motion rule requires of anything that is not a value the reader controls. Its consumer pairs it with a finite iteration count so the total motion stays under the five seconds at which WCAG 2.2 SC 2.2.2 engages. | skeleton |
--opsin-ease-spring does not resolve to a linear() value in this theme, and no spring parameters were given. Curves are generated from tokens/motion.json by scripts/build-tokens.mts.
--opsin-ease-spring. The dashed line is the target; anything above it is overshoot.Accessibility impact
The generated motion layer is itself an accessibility artefact, in three ways.
- The reduced variants are auditable from the generated source. Every token
that declares one records its
reducedMotionValueinlib/generated/tokens.ts, so the reduced behaviour of the system can be answered from one generated file rather than read out of every component. Surfacing it beside the full value in this table is still to do. - Settling durations are published. A long-settling spring on an interactive control means the control is still moving when somebody reaches for it; having the number in a table is what makes that reviewable.
- The table is a table. Real table semantics with a header row, not a grid of divs, so it can be navigated cell by cell and read column by column. That applies to every generated table on this site.
Related
- Springs as tokens shows how the spring rows in this table are produced, and what the damping ratio means.
- Reduced motion shows what each token becomes under the preference, and why a reduced variant is never simply "off".
- Reference → Tokens has the same rows alongside every other family, for when you are auditing rather than building.
Reduced motion
What each motion token becomes when a reader asks for less movement. A stated reduced variant per token, not a kill switch that leaves things teleporting.
Typography
Why opsinjs ships no webfont, what the platform UI font buys a health product, and the four rules every piece of type in the system obeys.