Skip to main content
Motion in Substrate is two authored values and one runtime multiplier. There is no duration ladder — no fast, normal, slow set to choose between — because a discrete ladder cannot express a user sitting halfway between full motion and none. A brand declares its base duration and its curve; the runtime scales the duration continuously.

The motion block

Both fields are required. An example, from the fictional Acme brand’s config.global.yaml:
  • duration-base — a bare number in milliseconds. This is the brand’s full-strength transition duration.
  • easing — a CSS easing function, as a string. It reaches the browser as --easing on the brand selector.
Different brands can sit far apart on both, which is the point — motion is brand character, not a system default. Here is how a fictional airline family, Aurora Airways, might spread its sub-brands: Aurora’s 75ms linear is a deliberate choice for a transactional booking flow; Aurora Corporate quadruples it to 300ms and swaps in an eased curve for a slower, more formal surface. Cargo overrides only duration-base and keeps the family’s linear easing — see Inheritance for how partial overrides merge.

The continuous model

The authored base is emitted per brand, and one global expression scales it. The shape of a brand’s generated css/brand.gen.css:
And from generated/global/css/tokens.gen.css:
That is the entire motion system. Components reference --duration (through --surface-transition-duration) and --easing; nothing references a named speed tier, because none exists.

The motion factor

motionFactor is one of the seven axes on the runtime preference vector, running from 0 (instant) to 1 (the brand’s full authored duration). Two things about it are worth knowing: It defaults to 0.75, not 1. The shipped default preference set deliberately runs motion at three-quarters strength. A brand that authors duration-base: 200 sees 150ms out of the box. It is user-adjustable, within brand-set bounds. The flexibility.motion block is the brand’s policy on how far the slider may travel:
Every bundled demo brand allows the full range. A brand could narrow it — { min: 0.5, max: 1 } would prevent users from disabling motion entirely — though allowing 0 is what makes the reduced-motion path work.

Reduced motion

Substrate does not emit a prefers-reduced-motion media block. The handling lives in the web runtime, which reads the media query and sets motionFactor to 0:
Because --duration is --duration-base times the factor, zeroing the factor zeroes every duration in the system at once — no per-token overrides, and no component-level checks. The runtime also listens for changes, so a user toggling the OS setting with the page open takes effect immediately rather than on next load.
Handling reduced motion through the preference vector rather than a media query is what lets the same mechanism serve a user who wants less motion rather than none. A media block is binary; the factor is continuous, so 0.3 is a reachable state.
If a transition feels wrong across the whole brand, change duration-base — it moves everything coherently. If one interaction feels wrong, the fix belongs in that component’s descriptor, not in a new brand-level duration value.

Brand Config Overview

The full schema, including the flexibility policy block that bounds every preference axis.

Core Concepts

The preference vector in full, and why values are computed at runtime.