The Token System

Cadence: Case Study · Chapter 1


Token Architecture

The live code view: actual Framer Motion calls with current token values, fixed references tagged (fixed).
The live code view. Values tick as sliders move; fixed references carry the (fixed) tag.

Tokens are CSS custom properties in one file, src/tokens/motion.css. Five families: duration (fast 100ms, base 200ms, slow 400ms, slower 600ms), easing (five named cubic-bezier curves), delay (a named zero plus short, medium, long), scale (three press compressions below 1 and a lift above it, renamed pressSubtle / pressBase / pressExpressive / lift in July so direction lives in the name and no key implies a neutral it does not have), and spring (stiffness, damping, and mass, unitless, because a real spring is not measured in time). Components never hardcode an animation value; they read tokens at runtime through getComputedStyle. The custom-property layer is the one Material and Primer ship; the runtime read is Cadence's addition, and it is what keeps the demonstration authentic rather than simulated: Token Lab edits the same layer a production system would ship.

The set splits into editable tokens and fixed references. The durations, the four easing slots, the delays, the scales, and the spring parameters all have controls, and each control section carries a Constrained / Explore toggle: Constrained keeps the ranges a shipping UI would use, Explore opens the full range. The fourth easing slot, ease.overshoot, surfaces its control only in Explore, where the curve graph has the vertical room its above-one handle needs. Two tokens deliberately have no control at all: ease.linear is the constant-velocity baseline every curve is measured against, and delay.none is the system's named zero. The live code view tags these reads (fixed), and a guard test asserts the two classes partition the full token set with nothing shared and nothing left over. One value stands outside the partition on purpose: the duration scalar, a single editable multiplier (effective duration = base × scalar) that drives the duration-versus-distance plot, handled outside the family machinery as a demonstrative multiplier.

Token sets export in four formats: the W3C Design Tokens Community Group format (DTCG, the $type/$value shape Style Dictionary and Figma Variables consume), flat JSON mirroring the CSS variable names, a drop-in CSS :root block, and a ready-to-use Framer Motion module (cadence.motion.js) with named exports in seconds, four-number ease arrays, the spring as a native { type: 'spring' } config, and composed transition examples exporting the motion-side artifact as well as the token-side. All four serialize from one normalized object so they cannot drift. The spring family posed the one format question, because the DTCG draft has no spring type: it serializes as three number leaves under a motion.spring group, valid today with no invented type, and the group name carries the composition a custom type would have added.

Import runs the pipeline in reverse with validation: scalars clamp to the Explore bounds and report, missing tokens fill from the Standard preset and report, round-tripped curves re-canonicalize to their named keys, and the report modal lists every correction. One class of value refuses the clamp: a spring stiffness, damping, or mass at zero or below is a spring that never settles, so it fails the import as a structural error instead of being bent into something that looks valid and is not. A tuned token set leaves the tool as the artifact an engineer's pipeline consumes.

The Hybrid Model: CSS Custom Properties + Framer Motion


The two-channel dispatch THE TWO-CHANNEL DISPATCH slider drag SET_DURATION · fast · 140 dispatch(action) CHANNEL 1 · CSS syncToCss(action) --motion-duration-fast written on :root anything reading the document sees the new value CHANNEL 2 · REACT STATE reducer state stateToTokens(state) → MotionTokensProvider demo components take the same values with no CSS read at all One dispatch, both channels, no divergence: the context object is derived from the same state that wrote the CSS.

CSS owns the values; Framer Motion executes the motion. The trick is keeping both honest while a slider is being dragged.

Token Lab runs a two-channel update. Every change writes the CSS custom property, so anything reading the document root sees the new value. Simultaneously, a React context provider hands the same values directly to components inside the demo area, bypassing the CSS read entirely, so a drag retimes the demos on the same frame instead of waiting for a re-read. Components outside the provider fall back to the CSS channel. One dispatch, both channels, no divergence: the context object is derived from the same state that wrote the CSS.

The system also draws a line the demonstration depends on. Demonstration motion, the thing a principle teaches, reads the editable --motion-* tokens, because the point is that editing a token changes it. The tool's own chrome (the interface around the content rather than the content itself: hover states, the nav crossfade, accordions) reads fixed --feedback-* constants instead, so dragging a duration to near zero in Explore mode can never collapse the interface's own feedback into nothing. A build-gating test enforces the whole arrangement: an undocumented inline animation literal in a component fails the suite, and the one literal on its allow list is Token Fidelity's deviant pill, hardcoded because that principle teaches what hardcoding does.


The Spring That Is Not a Curve

For the first three months of development the token set carried an easing curve named spring: (0.34, 1.56, 0.64, 1), a cubic-bezier whose control point climbs past 1 and comes back down. It gives the look of a spring on a fixed timeline. A true spring has no duration; you give it stiffness, damping, and mass, and the settle time falls out of those three. A harmonization pass renamed the bezier to overshoot: the name now describes the curve function, not what it imitates.

The rename freed the name up for instituting the real thing. Three unitless custom properties join the token layer, read at runtime like every other token, and Framer Motion consumes them as { type: 'spring', stiffness, damping, mass } instead of a duration plus a curve. Each of the three built-in presets bakes its own spring personality: Snappy is stiff and bounces hard, Cinematic damps the bounce nearly out and arrives composed, Standard settles with a hint of ring. Material 3 Expressive moved its expressive motion to physics springs in 2025; Cadence follows without breaking its own read-at-runtime rule, because unitless numbers are compatible in CSS custom properties.

Two toggles flip on one shared state: the left rides duration.fast on ease.overshoot, the right rides the real spring, each with its curve graphic above.
The imitation and the physics on one state. Each toggle carries its curve above it; the spring has no duration anywhere.

The tool surface makes the difference visible. The Spring section carries three sliders and a settle-curve visualizer: a plot of displacement over time, rising, overshooting the target, settling, redrawn as the sliders move. The math underneath is the damped harmonic oscillator, the same second-order system Framer Motion integrates, kept in a pure module so the three regimes (underdamped rings, critical arrives clean, overdamped crawls) test without React. Switch a Button to Spring, drag stiffness, and the button, the chart, and the dedicated SpringDemo move together off one context.

One gap surfaced on the way. Reduced-motion support here works by flattening durations to near zero, and a spring has no duration to flatten, so the preference slid right past it. The flattened token set now carries a flag the spring consumers read, falling back to the bezier branch whose timing is already collapsed. The principle demo that forced the fix is Follow Through, the first reduced-motion-respecting surface to run the real spring.


Companion: Two Lexicons, the same engineering organized as a translation table between motion design and design engineering.

← Case Study · The Principles →