opsinjs
FoundationsToken familiesMotion

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

TokenWhat it controlsUsed 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-snap283msSettle 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-settle382msSettle 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-calm550msSettle 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-sheet483msSettle 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-instant80msState change with no travel: hover tint, focus ring, checkbox tick.no component
--opsin-duration-fast140msSmall 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-base220msThe default for chrome that is not spring-driven.dialog, sheet
--opsin-duration-slow360msLayout change: a list reflowing, a card expanding.no component
--opsin-duration-deliberate560msThe 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-shimmer1600msOne 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 reducedMotionValue in lib/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.
  • 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.

On this page