Command Palette

Search for a command to run...

Hectal
Shots in this scene
SHOT 18.1Advanced ~23 min · shot 1 of 6

Motion Tokens

In one line

A motion token is a named value (a duration, an easing or a spring) that components use instead of typing the number. Change it once and every component follows; name it by purpose (enter, exit, emphasis) and nobody has to remember numbers.

0/6 · 0%

Think of it like this

A restaurant chain gives every branch the same recipe card: "sear 90 seconds". Cooks read the card; they do not guess. When head office finds 75 seconds is better, one card changes and every kitchen follows. Motion tokens are the recipe card for a site's movement.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

motion token
A named motion value (duration, easing or spring numbers) kept in one place so components share it.
primitive and purpose token
A primitive says how big (--dur-base is 220 ms). A purpose token says what it is for (--motion-enter-duration) and points at a primitive.
damping ratio
How strongly a spring resists moving, as a shape: 1 settles without overshoot, below 1 overshoots.
single source of truth
One place where a value is written. Everything else reads it, so nothing can disagree.

Step by step

01Find the numbers the site already uses

Search your CSS for times. Most are fallbacks inside var(--dur-base, 220ms), already tokens. The rest (the logo's 700, 500 and 350 ms) belong to a one-off choreography and stay.

terminal
$ grep -rnoE "[0-9.]+ms" src --include=*.css | grep -v tokens.css
── expected output ──
src/styles/components.css:79:220ms
src/styles/components.css:80:120ms
src/styles/components.css:174:220ms
src/styles/components.css:175:400ms
src/styles/components.css:181:120ms
src/styles/components.css:182:220ms
src/styles/components.css:189:120ms
src/styles/components.css:193:120ms
src/styles/components.css:212:700ms
src/styles/components.css:213:500ms
src/styles/components.css:213:350ms
src/styles/components.css:214:400ms
src/styles/components.css:214:700ms
src/styles/scroll.css:28:400ms
src/styles/scroll.css:28:400ms
Run on the project assembled from the build files of Phases 0 to 13 and 17 (line numbers in yours differ if you added rules). Lines 212 to 214 are the logo's own choreography (Phase 13); every other line is a fallback inside `var(--dur-base, 220ms)` and its cousins. The drawer and the gallery use springs, so they have no millisecond value in CSS at all.

02Add the purpose and spring tokens

In tokens.css add the purpose tokens, each pointing at a primitive, and four spring tokens written "stiffness ratio". A component then asks for a purpose: transition: opacity var(--motion-enter-duration) var(--motion-enter-ease).

03Add tokens.js

Create src/js/motion/tokens.js with token(), ms() and springOf(). A wrong token must throw with its name.

04Read the numbers

Run the third example. All three spatial springs overshoot by the same 0.15 %, whatever their stiffness (only the ratio decides it), the effects springs by 0 %, and Lumen's drawer by 0.39 % (about 1.2 px on a 320 px drawer).

Try it yourself

  1. 1

    Read the three tracks

    In "One token, many components" press Pause, Play all three, then Step to 110 ms. In our run the toast (220 ms) was at opacity 0.96, the drawer 182 px from its place (400 ms) and the chip (120 ms) at 0.40.

  2. 2

    Change one primitive

    Reload the example, press --dur-base: 440ms, then Play all three. Only the toast's two tracks (transition · opacity and transition · transform) become 440 ms; the drawer stays 400 ms and the chip 120 ms. With All three ×2 the drawer is 800 ms and the chip 240 ms.

Code & diagrams

Try it: interactive

A spring token as sliders

Set stiffness 380 and damping 34 (the drawer, ratio 0.87). Then try damping 20 (ratio 0.51) and read the overshoot: about 15 %.

One token, many components

The toast (enter), drawer (emphasis) and chip (exit) use purpose tokens. The script reads their durations, changes primitives on the root and reads again. Press **Play all three**, then the buttons.

Press Run to see the page.
00:00.000
animations
0.00s0.10s0.20s0.30s0.40s0.50s0.60s0.70s0.80s0.90s

Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.

frames

Console

Nothing logged yet.

Expected console output

as shipped: toast 220 ms, drawer 400 ms, chip 120 ms
--dur-base 440ms: toast 440 ms, drawer 400 ms, chip 120 ms
all three doubled: toast 440 ms, drawer 800 ms, chip 240 ms
The same tokens in JavaScript

tokens.js inlined. A `var()` token resolves, `0.4s` becomes 400, and a unit-less or missing token throws an error that names it.

Press Run to see the page.
00:00.000
animations
0.00s0.10s0.20s0.30s0.40s0.50s0.60s0.70s0.80s0.90s

Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.

frames

Console

Nothing logged yet.

Expected console output

raw getPropertyValue("--dur-base") -> "220ms"
ms("--dur-base") -> 220
ms("--motion-enter-duration") -> 220
ms("--dur-long") -> 400
ms("--dur-typo") -> RangeError: motion token --dur-typo is not a duration: "220"
ms("--dur-nope") -> RangeError: motion token --dur-nope is not a duration: ""
springOf("--spring-spatial") -> {"stiffness":380,"mass":1,"damping":33.91884431993519}
springOf("--dur-base") -> RangeError: motion token --dur-base is not "stiffness ratio": "220ms"
Spring tokens in numbers

Each token runs from 0 to 1 in the loop of spring.js, on a finer step. Rest: closer than 0.1 % and slower than 0.01 per second. Overshoot comes from the exact formula `exp(-π·ζ / √(1−ζ²))`. Material's values: @material/web 2.5.0.

Press Run to see the page.
00:00.000
animations
0.00s0.10s0.20s0.30s0.40s0.50s0.60s0.70s0.80s0.90s

Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.

frames

Console

Nothing logged yet.

Expected console output

Material spatial fast      stiffness 1400  ratio 0.9  overshoot  0.15 %  rests after about 260 ms
Material spatial default   stiffness  700  ratio 0.9  overshoot  0.15 %  rests after about 350 ms
Material spatial slow      stiffness  300  ratio 0.9  overshoot  0.15 %  rests after about 480 ms
Material effects fast      stiffness 3800  ratio 1  overshoot  0.00 %  rests after about 190 ms
Material effects default   stiffness 1600  ratio 1  overshoot  0.00 %  rests after about 270 ms
Material effects slow      stiffness  800  ratio 1  overshoot  0.00 %  rests after about 370 ms
Lumen spatial (the drawer) stiffness  380  ratio 0.87  overshoot  0.39 %  rests after about 490 ms
Lumen effects              stiffness 1600  ratio 1  overshoot  0.00 %  rests after about 270 ms

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Misspell a primitive inside a purpose token

In "One token, many components" change --motion-enter-duration: var(--dur-base); to var(--dur-bse).

terminal
$ Run the example and read the console
── what you'll see ──
as shipped: toast 0 ms, drawer 400 ms, chip 120 ms
--dur-base 440ms: toast 0 ms, drawer 400 ms, chip 120 ms
all three doubled: toast 0 ms, drawer 800 ms, chip 240 ms

Myth vs fact

Myth

Tokens are just variables, so they add nothing.

Fact

The names are the point. A purpose name tells a developer which duration to use and tells a reviewer when it is wrong.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    getPropertyValue() on an unregistered custom property returns resolved text, so var() chains work, but nothing checks the value. Registering a token with @property and syntax: "<time>" (Topic 17.5) makes the browser reject a wrong value, at the cost of a rule per token. We keep tokens plain and test them.

  • ▸

    Computed transitionDuration comes back as "0.22s", so comparing it with "220ms" is wrong. Read the token when you need the number you wrote.

  • ▸

    A ratio also makes the overshoot a property of the token alone: exp(-π·ζ / √(1−ζ²)) has no stiffness in it, so 0.9 overshoots 0.15 % at stiffness 300 or 1400. spring.js steps at 1/240 s with semi-implicit Euler, which under-reports that slightly: the drawer's 0.39 % reads about 0.13 % there (we ran both). That is under half a pixel on a 320 px drawer.

Remember this

  1. 1

    The problem this solves: the Lumen site has many animations written by many hands. If each types its own 220ms or 0.25s, nobody can say what "the speed of Lumen" is, changing the feel means searching every file, and no test can check that two components agree. A token gives each number one name and one home.

  2. 2

    Two layers. The primitives from Phase 2 (--dur-fast, --dur-base, --dur-slow, --ease-out, --ease-in, --ease-in-out) say how big something is. The new purpose tokens say what the motion is for: --motion-enter-* for things that appear, --motion-exit-* for things that leave, --motion-emphasis-* for a big, rare move. A purpose token points at a primitive with var(). A component asks for a purpose and never picks a number, so designers can change what "enter" means without touching a component. The defaults follow Topics 2.4 and 5.3: arrive in 220 ms easing out, leave in 120 ms easing in.

  3. 3

    Springs can be tokens too. CSS cannot run a spring, but a custom property can store its numbers for JavaScript. Material 3 made springs tokens: a stiffness plus a damping ratio, in two kinds (spatial for things that move, effects for opacity and colour) and three speeds. Its values in @material/web 2.5.0 (md-sys-motion.scss, standard scheme) are spatial 1400, 700 and 300 with damping 0.9, and effects 3800, 1600 and 800 with damping 1. Lumen copies the shape: --spring-spatial: 380 0.87 is the drawer's spring from Phase 9.

  4. 4

    Why a ratio. spring() takes damping in the same units as stiffness, so damping: 34 means a different shape at a different stiffness. The damping ratio is the shape: 1 settles with no overshoot, below 1 overshoots a little. damping = ratio × 2 × √(stiffness × mass); the drawer's 380 and 34 give 0.87.

  5. 5

    The JavaScript mirror. tokens.js reads the same custom properties with getComputedStyle(document.documentElement).getPropertyValue(name), so CSS stays the single source of truth. A value written with var() comes back resolved. ms() turns 220ms or 0.4s into milliseconds; springOf() turns 380 0.87 into the options spring() takes. Both **throw a RangeError naming the token** when it is missing or wrong: a NaN would quietly make a zero-length animation.

  6. 6

    What tokens do not do. They do not replace the reduced-motion rules of Phase 7 (a duration cannot turn a slide into a fade), and a one-off choreography like the logo's 700, 500 and 350 ms draw (Phase 13) stays in its own rule.

Explain it without notes

01

Why use purpose tokens on top of primitives?

02

Why does a spring token store a damping ratio?

03

What does ms("--dur-nope") do, and why is that better than NaN?

Practice

01

Add a tooltip to the first example that slides 12 px and fades using the enter tokens. Which button changes its duration?

02

Write easeOf(name) for tokens.js: it returns the token's text and throws a RangeError when the token is empty.

Done when you can

  • You can explain primitive, purpose and spring tokens, and why components use only purposes

  • You can read a token in JavaScript and get a number, or a clear error

  • You can say what a damping ratio of 0.87 and of 1 mean

  • You know a token does not replace a reduced-motion rule