Reduced Motion in Design Systems and Tokens
A reduced-motion override written component by component is a list that is never finished: it covers what existed when it was written and misses everything added afterwards. Expressing motion as design tokens changes the shape of the problem β one override at the token layer reaches every component that consumes them, including ones that do not exist yet. This guide covers the token structure and what the override should do to each part of it. It sits under implementing prefers-reduced-motion.
When to use this approach
- Any project with a design system. The tokens probably already exist for colour and spacing; motion is the category most often left out.
- A codebase where the reduced-motion override keeps needing additions. That is the symptom this solves.
- Before building a motion-heavy section. Tokenising afterwards means auditing every literal value in the codebase.
- Alongside a motion intensity control. A scale control needs tokenised values to multiply; without them it reaches nothing.
Implementation
1. Define a small motion scale
:root {
--motion-distance-sm: 8px;
--motion-distance-md: 16px;
--motion-distance-lg: 24px;
--motion-fast: 120ms;
--motion-base: 240ms;
--motion-slow: 400ms;
}
Three steps in each dimension is enough for almost any interface. More steps produce inconsistency rather than nuance, because nobody can reliably choose between seven similar values.
2. Consume the tokens everywhere
.card {
transition: translate var(--motion-base) ease, opacity var(--motion-base) ease;
}
.card:hover { translate: 0 calc(-1 * var(--motion-distance-sm)); }
.reveal {
translate: 0 var(--motion-distance-md);
animation: rise linear both;
animation-timeline: view(block);
}
The rule to hold is that no component contains a literal distance or duration. A literal is a place the override will not reach, and it will be found by a user rather than by a search.
3. Write one override
@media (prefers-reduced-motion: reduce) {
:root {
--motion-distance-sm: 0px;
--motion-distance-md: 0px;
--motion-distance-lg: 0px;
--motion-fast: 60ms;
--motion-base: 100ms;
--motion-slow: 120ms;
}
}
Distances go to zero because displacement is what causes harm. Durations shrink rather than disappearing, because an instant state change is less comprehensible than a quick one and opacity is not a trigger.
4. Reset what tokens cannot reach
@media (prefers-reduced-motion: reduce) {
* {
animation-timeline: auto !important;
animation-range: normal !important;
}
}
Timeline attachment is not a value a token can carry, so scroll-driven animations still need an explicit reset. Keeping that reset short and in one place is the counterpart to the token override rather than a replacement for it.
5. Document the scale, including what it means
A token named --motion-distance-md with no stated purpose gets used for whatever is nearby. A token documented as βan arrival β reveals and list entriesβ gets used for arrivals, which is what makes the scale hold its shape over time.
Verification
Search the codebase for literal durations and pixel translations inside transition and animation declarations. Every hit is a place the override does not reach, and the list should be empty.
Then set the preference and walk the component library. Every component should be static in position and quick in state change; anything still moving is either a literal or a scroll-driven animation whose timeline needs the explicit reset.
Add a check that fails the build on a literal ms value inside a transition or animation shorthand in component styles. It is a crude rule with occasional false positives, and it keeps the system from drifting back.
Finally, confirm the durations did not go to zero. A page where every state change is instant under reduced motion is a page that has removed the feedback along with the motion, and it is a common over-correction.
Edge cases and gotchas
Tokens defined per component rather than globally. A component that redefines --motion-base locally is opting out of the override for itself, usually accidentally.
Third-party components. They carry their own values and the token override does not reach them. Where they expose configuration, pass the preference through; where they do not, a targeted reset is the only option.
Design tools exporting literal values. Token pipelines that flatten custom properties into computed values at build time defeat the entire approach. The tokens must survive to runtime as custom properties.
@property registration for interpolable tokens. A duration token used in calc() needs registration for the arithmetic to resolve, in the same way a motion scale does.
Motion tokens used for non-motion purposes. A duration token borrowed for a debounce interval or a timeout will be shortened by the override, with results nobody intended.
Browser-specific notes
Custom properties and the reduced-motion query are supported everywhere relevant and have been for years, so the approach itself needs no guards.
The one behaviour worth knowing is that overriding a custom property inside a media query re-resolves every declaration that consumes it, in every engine, without any additional work. That is what makes the single-override approach viable rather than merely tidy.
Where the engines differ is in how the preference is delivered by the platform, which is covered in the motion preference support matrix. Nothing in the token architecture changes with the engine.
Frequently Asked Questions
Should distances really go all the way to zero?
For most interfaces, yes. Displacement is the characteristic that causes vestibular symptoms, and a reduced displacement is still displacement. The exception is where a small movement is the only thing distinguishing two states β a toggle, a drag affordance β and there the honest answer is usually to add a non-motion cue rather than to keep a residual movement.
What about a motion scale control alongside this?
They compose well: the token override handles the system preference, and a scale control multiplies the same tokens. Expressing distances as calc(var(--motion-distance-md) * var(--motion-scale)) gives both, with the stored user choice taking priority over the media query.
Does this replace per-component review?
No, but it changes what the review is for. With tokens in place, the question at review is whether a component uses the right step for what it is expressing β not whether it will respond to the preference at all, which the architecture now guarantees.
How do I introduce this to an existing codebase?
Incrementally, starting with the components that carry the most motion. Define the scale, convert the three or four heaviest components, and add the build check so nothing new arrives with literals. The remaining scatter of small transitions can be converted as those files are touched for other reasons.
Where should the motion tokens live relative to the rest of the system?
Alongside spacing and colour, in the same file and with the same review process. Motion treated as a separate concern owned by whoever is building the current animated section is motion that will be inconsistent, and inconsistency in movement reads as sloppiness far more quickly than inconsistency in, say, border radius.
Putting them together also means the reduced-motion override sits in the same place as the dark-theme override and any other systemic variation, which makes the pattern familiar rather than special.
How do I stop new components introducing literals?
A build-time check is the only mechanism that survives. A rule that fails on a literal duration inside a transition or animation declaration in component styles is crude, has occasional legitimate exceptions, and is dramatically more effective than review β because review catches the literals someone notices, and the check catches all of them.
Pair it with an allow-list for the handful of genuine exceptions rather than weakening the rule, so each exception is a decision someone made rather than a gap.
Do easing curves belong in the token set?
Two or three of them, yes β an ease-out for arrivals, a symmetric ease for state changes, and linear for anything scroll-driven. Beyond that the set stops being a system and becomes a collection, and the reduced-motion override has nothing to do with them anyway since easing is not displacement.
Related
- Implementing prefers-reduced-motion β the override this architecture serves
- Motion Scaling & User Preferences β the graduated control that consumes the same tokens
- Vestibular-Safe Motion Design β where the distance caps come from
- Accessible Transitions: Focus & Announcements β the work tokens cannot express