The preference vector
Every axis is an independent, continuous scalar (cvd being a small struct rather than a number). These are the real defaults from defaultPreferences():
Because the axes are independent, they compose freely. A user who wants a dark interface with elevated contrast, reduced motion, and deutan compensation is not choosing a fourth “mode” — they are setting four numbers.
There is no theme attribute in Substrate.
scheme is a continuous variable, not a string; the runtime writes data-brand on the root element; and data-mode is a composable mode list (intent roles, component roles and states, preset-level and material tokens) — a preset token applies property deltas, it does not move the scheme axis. See Applying a mode below.The five presets
Named modes ship in the engine’s scheme-preset table (src/kernel/system/preferences.ts). Each one sets exactly two axes — scheme and contrastFactor — and leaves the rest of the vector untouched:
The keys are camelCase. There is no
high-contrast and no high-contrast-dark.
Applying a preset is a two-field assignment; nothing about the other axes changes:
scheme: 0.42 is just as valid as a named one.
Applying a mode
On the web, changing a mode means changing the preference vector and re-running the solver. The runtime writes the solved primitives back to CSS custom properties, and the cascade updates:scheme of 0.42 works identically. Full setup lives in the Quickstart and the Runtime & Imports page.
Elements opt into the resulting cascade with data-ucs, and pick an intent role with data-mode:
data-mode is matched with ~= against a space-separated token list, so an element can carry several mode tokens at once — intent roles, component roles and states, brand preset levels, materials. Tokens apply property effects; the scheme axis itself stays on the preference vector. The full token vocabulary is on Markup Opt-In.
The no-JS floor
Substrate bakes a static snapshot into each brand’s CSS so a server-rendered page paints accessibly before hydration. That snapshot is a 2×2 matrix — light/dark × normal/more contrast — emitted on[data-brand="<slug>"] and gated by media queries:
- Continuous
schemepositions — includingdimmed— and arbitrarycontrastFactorvalues - CVD compensation,
warmth,densityFactor, and the other continuous axes - Nested and scoped surfaces, where contrast is re-solved against the local background rather than the page
prefers-reduced-motion drops motionFactor to 0, prefers-contrast: more raises contrastFactor to 1.3 while prefers-contrast: less lowers it to 0.85, and forced-colors: active disables CVD compensation so the OS palette wins.
Light & Dark
How the scheme axis works: track evaluation, polarity flip, and continuous positions between the endpoints.
High Contrast
contrastFactor as a multiplier over the Lc policy, and how the runtime picks it up from the OS.Color Vision
The four CVD types, continuous severity, hue redistribution, and the kernel’s own caveats.
Runtime & Imports
The runtime call sequence and the three import aliases this page’s vector drives.
Surface Component
Re-solving contrast locally when surfaces nest inside surfaces.