Motion
Motion explains a change of place. The sheet slides, the camera eases to a selection, a switch's thumb slides to its other end, a toast slides in and out, and meters and progress bars ease to new values. Vehicles glide between reports instead of jumping.
Durations
- Transitions move something from one place or value to another, once:
duration.sheet,duration.meter,duration.camera,duration.follow,duration.switchandduration.toast. A control pressed many times in a row, such as a switch, takes the shortduration.switch, so the change keeps up with the hand. A toast slides up into its place and fades in onduration.toast, and slides down or away in the direction of its swipe as it leaves, so a message that arrives at the screen's edge is seen arriving. - Loops repeat while something is true. A new report pulses once around its vehicle (
duration.report-pulse), a ring pulses around the selected vehicle (duration.selection-pulse), the follow indicator blinks while the camera follows (duration.blink), and a segment sweeps along a progress bar's track while how far its task has got is not known (duration.progress-sweep).
| Token | Value | Reduced motion |
|---|---|---|
duration.sheetBottom sheet open and close | 320 ms | 0.01 ms |
duration.meterMeter and progress bar fill change | 800 ms | 0.01 ms |
duration.cameraHow long the map camera takes to fly to a selection | 750 ms | 0.01 ms |
duration.followCamera fly-to when following starts | 500 ms | 0.01 ms |
duration.switchSwitch thumb slide and track fill | 150 ms | 0.01 ms |
duration.toastToast slide and fade in and out | 200 ms | 0.01 ms |
duration.blinkRecording dot blink while following | 1400 ms | |
duration.report-pulseRing that expands when a vehicle reports its position | 1100 ms | |
duration.selection-pulseRing around the selected vehicle | 1600 ms | |
duration.progress-sweepSegment that sweeps along an indeterminate progress bar's track | 1600 ms |
Easing
The sheet has its own curve, which settles slowly so a long slide doesn't stop abruptly. Everything else eases with easing.standard, the browser's ease.
| Token | Value |
|---|---|
easing.sheetCurve of the bottom sheet opening and closing | cubic-bezier(0.2, 0.8, 0.2, 1) |
easing.standardCSS ease (meters, the switch) | cubic-bezier(0.25, 0.1, 0.25, 1) |
Reduced motion
When the system asks for less motion, the transition tokens take 0.01 ms: their values under prefers-reduced-motion: reduce are in the table's Reduced motion column. The value is 0.01 ms rather than 0, so transitionend and animation events still fire. CSS that reads the tokens needs nothing more.
- Loops keep their durations in the tokens, because a loop at 0.01 ms would flicker. Each component that runs a loop stops it under reduced motion.
- Vehicles move to each new report without gliding.
The values come from the resolver's motion modifier. Its default context, full, is empty, because base already holds the full-motion values; its reduced context points at packages/perron/tokens/motion-reduced.tokens.json. The build writes the overrides into each output:
| Output | Reduced motion |
|---|---|
tokens.css | The overrides inside @media (prefers-reduced-motion: reduce), on :root |
tokens.ts | modifiers.motion.reduced, as CSS text and as numbers |
penpot.tokens.json | A motion-reduced set, and a Motion group with Full and Reduced themes |
Timings in script
Script that animates, such as the map camera's fly-to, reads numeric from tokens.ts, and modifiers.motion.reduced.numeric when matchMedia('(prefers-reduced-motion: reduce)') matches. Both hold durations in milliseconds.