Skip to main content
Most design systems ship modes as parallel artifacts: a light token set, a dark token set, and a switch that swaps one for the other. Substrate does not work that way. It models user context as a continuous preference vector with seven axes, and a “mode” is nothing more than a named preset — a convenient pair of coordinates on that continuum. Everything between the presets is reachable, and the values at every point are solved, not looked up. This page covers the preference vector, the five shipped presets, how the runtime applies them, and what the no-JS fallback does and does not cover. For the underlying mental model, see Core Concepts.

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:
The preset table is a convenience, not a gate. Assigning the same two numbers directly is exactly equivalent, which is why an arbitrary position such as scheme: 0.42 is just as valid as a named one.
dimmed is the one preset with no baked native artifact. Because it sits mid-track, it can only be produced by sampling the track continuously through the kernel. Light, dark, highContrast, and darkHighContrast each have a static Swift and Compose file; dimmed does not. See iOS / Swift and Android / Kotlin.

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:
Nothing about that call sequence is specific to a preset — an arbitrary 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:
This is the accessible floor, resolved for the page/root surface only. JavaScript is the primary path, and these all require it:
  • Continuous scheme positions — including dimmed — and arbitrary contrastFactor values
  • 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
The runtime also reads OS signals directly: 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.