The Problem With Static Tokens
Static design tokens store a value.color-primary-500: #1A7F5A. That value was probably chosen to look good in light mode, on a calibrated display, by someone with standard color vision. It may pass a WCAG 2.x contrast check against a white background. But it says nothing about:
- Dark surfaces — the same hue at the same lightness often fails contrast on dark backgrounds, where APCA polarity flips.
- In-between states — real preferences aren’t binary. A comfortable low-light reading position sits between light and dark; a static ramp has no value there.
- High-contrast needs — users with low vision need foregrounds solved to genuinely higher contrast targets, not a slightly bolder version of the same token.
- Color vision deficiency (CVD) — a palette designed around green and red as signal colors fails a large portion of users without a CVD-aware remap.
- Density, type scale, and motion sensitivity — spacing, sizing, and animation are preferences too, and ladders of fixed steps can’t follow them continuously.
How Substrate Works
1
Declare your brand intent
Author a YAML brand config: an open map of intents, each just a hue and chroma — lightness is never authored. Add your spatial unit, type ratio, motion, elevation, and the flexibility bounds users may adjust within.
2
Generate solver inputs and artifacts
The pipeline compiles the CSS cascade, component style descriptors, and per-mode baked artifacts for every brand — plus Swift, Kotlin, DTCG, and React Native outputs.
3
Solve at runtime
A small kernel (JS on the web; Swift and Kotlin ports on mobile) solves APCA contrast per surface under the user’s preference vector and writes the results as
--ucs-* variables. Change a preference and the whole interface re-solves — no rebuild.Key Benefits
Always-accessible contrast
Always-accessible contrast
Every foreground in every context meets its APCA target by construction — foreground Lc 75, borders Lc 50, focus rings Lc 60, scaled by the user’s contrast preference. Shortfalls are reported explicitly, and a build-failing accessibility gate keeps every brand in the build honest.
Continuous adaptation, presets included
Continuous adaptation, presets included
One brand config serves the whole preference continuum. Named presets —
light, dark, dimmed, highContrast, darkHighContrast — are convenience points, and users can sit anywhere between them. You don’t maintain parallel files.Color vision deficiency compensation
Color vision deficiency compensation
CVD support is a runtime preference (
protan, deutan, tritan, achromat, with continuous severity): intent hues are redistributed onto a CVD-safe arc before contrast solving, and achromat mode adds pattern overlays so meaning never rides on color alone.Multi-brand from a single config model
Multi-brand from a single config model
Brand families share a base config; sub-brands author only deltas and inherit everything else through a deep-merge cascade. All accessibility behavior comes with the model.
Platform-agnostic output
Platform-agnostic output
Every target — Web (CSS + JS runtime), iOS (Swift, Display P3), Android (Kotlin/Compose), React Native, DTCG JSON for design tools, xcassets — derives from the same brand data.
Where to Go Next
Quickstart
Vendor the engine, run substrate init, and initialize the runtime solver.
Core Concepts
Dive deeper into color intent, runtime APCA solving, and cascading inheritance.
Runtime & Imports
The required app wiring: the import contract and the three-call runtime sequence.
Surface Component
The one shipped component — required wherever surfaces nest inside surfaces.