src/components/ with a fixed file layout:
config.yaml is the styling config this page documents; config.doc.yaml is prose metadata (description, anatomy, accessibility, guidelines). A third file shape, config.doc.gen.yaml, is a generated per-brand projection — derived, never edited.
The vocabulary
The engine defines these terms precisely, and two of them are counter-intuitive:
A documentation page may describe fewer roles than the config declares — never one the config does not.
A complete config
The badge config, verbatim from the engine — small enough to read whole, and it exercises the core schema:extends: types/actionpulls in a shared archetype — roles, states, and semantic maps defined once in a type config. The value is a path (types/action), not a bare name.states: nulldeletes. Badge inherits hover/pressed states from the action archetype, then removes them, because a badge is non-interactive.nullis the only delete signal in the cascade — there is no separate “unset” keyword.- Relative deltas are quoted strings. Where a state nudges a channel, it authors
lightness: "+0.05"— quoted so YAML preserves the sign and the value stays a delta rather than an absolute.
Channels: background, foreground, border
background, foreground, and border are channels, not ordinary properties. A channel accepts either a scalar shorthand or a block of channel keys — lightness, chroma, contrast, alpha, tint, gradient, filter, noise:
foreground: { contrast: auto } means auto-contrast — black on light, white on dark — computed from the resolved background rather than the APCA solve.
Spatial values are multipliers, not pixels
Spacing properties (padding-x, padding-y, gap-column, margins) are authored as unitless multipliers of the brand’s spatial system. padding-x: 2 generates:
calc() chain, with the resolved-at-defaults pixel value as a comment. Similarly, font-scale is a scale-step exponent, not a size: font-scale: -1 means one step below the base on the brand’s type ratio, computed as base × ratio^step (with a density-coupling term) — the same continuous model as everywhere else in Substrate. shape: full is the capsule shorthand, emitting border-radius: 9999px.
Parts and uses:
Parts declare the structure a component owns. The card component’s parts, from the engine:
uses:. The match is by part name against the type’s roles, with a suffix fallback — a part named emphasized-body with uses: types/text inherits the body role. An unmatched uses: is a hard error at generation, with a message that names the available roles and suggests the nearest one — the engine’s stated policy is converting silent no-ops (“the part would silently inherit nothing”) into loud failures.
The resolution cascade
A component’s final config is a deep merge across up to six layers, in a fixed order: base → type → brand type → sub-brand type → brand → sub-brand Brand overrides live either as a colocatedbrands/{brand}/config.yaml inside the component directory or in the brand’s own tree, and they are deltas only — a partial YAML containing exactly what changes, conventionally opened with a comment naming the brand, the component, and the design source. The merge rules are the same everywhere:
Keys are normalized kebab-case → camelCase for the kernel, with one carve-out: named-resource keys (
intents, materials, gradients, ramps) round-trip verbatim, so your intent named acme-cyan stays acme-cyan. A key collision after normalization throws rather than silently dropping a spelling.
Validation: the generator refuses bad configs
Validation runs at generation time, and the policy is strict: unknown property keys, invalid value shapes, misplaced channel blocks, and unresolvable gradient references are hard errors with actionable diagnostics — pathed to the exact key (parts.header.style.margin-y-top) and carrying “did you mean” suggestions:
npm run build verifies generated output is current before compiling — stale output fails the build.
From config to CSS
The mapping to generated output is direct. For the badge config above, the generator emitsgenerated/global/css/components/badge/badge.gen.css (marked DO NOT EDIT):
Roles and modes both emit
[data-mode~="…"] selectors from the same code path — at the CSS level, a role name and a mode name are just tokens in the same space-separated attribute. That is why the markup contract treats data-mode as one composable mode list. A dev-time helper additionally watches the DOM and warns on data-mode tokens no config defines.
One naming trap. The Surface component’s config declares the roles
card, panel, and inset — a pure containment vocabulary with no extends: and an empty root style. The card component is a different artifact: it extends the feedback archetype and uses the conventional primary/secondary/auxiliary roles. When you see data-mode~="card", that is the surface role, not the card component.Type Configs
The five shared archetypes components inherit via extends — including the text roles that drive per-role contrast.
Markup Opt-In
How the roles, states, and modes defined here are activated per element with data-mode.