The Contract

Components do not carry their own colors, spacing, or type scale. They consume global design tokens:--color-primary,--space-m,--font-sand their relatives, defined once in your site's_design-tokens.css. That single file is the vocabulary, and it is how thirty installed components end up looking like one site instead of thirty.

The vocabulary is also a promise, and the promise runs in both directions. The moment a canon component consumes a token, every consuming site must define that token, and must keep defining it for as long as the component is installed anywhere. So:

  • Canon never references a token the vocabulary has not shipped.
  • The vocabulary never renames or drops a token canon consumes.

Neither side can see the other's working tree, which is why the contract needs to be auditable rather than remembered. Both halves of the tooling are covered at the end of this post.

The cost of the promise is what shapes the rest of the contract. Adding a token to the vocabulary is easy; removing one is a breaking change for every site that ever installed a component consuming it. That asymmetry is why the vocabulary should stay small, and it is why the next section exists.

Component Properties Are Not Tokens

Not every adjustable value deserves a place in the vocabulary. The decision rule is about who cares: a knob that several components share describes the design system and belongs in the vocabulary; a knob that exactly one component cares about describes that component, and putting it in the vocabulary would make every site guarantee it forever for one consumer's benefit.

A single-component knob becomes a component property instead: a custom property in the component's own--<component>-namespace, declared by the component on its root, with the default right there in the declaration.

css
.iframe-frame {
  --iframe-height: 50rem;
}

.iframe-frame iframe {
  block-size: var(--iframe-height);
}

The component defines it, so no site owes it anything. The name carries the owner, so a stylesheet full of custom properties still reads unambiguously. And retuning it is two lines inlib/overrides/iframe/iframe.css, which survives every future reinstall untouched. The override mechanics, and how component properties form each component's design API, are the subject of Customizing Components Without Editing Them.

Where a component property represents a design-system value, its default resolves to the corresponding token rather than repeating the literal, so the component picks up your site's design system while remaining individually tunable:

css
.logos-list {
  --logos-list-animation-speed: 15s;
  --logos-list-logo-padding: var(--space-s);
}

The prefix rule is not a style preference. An unprefixed property like--animation-speedsquats on a name any other component, or your site, might also want, and the collision resolves by cascade accident rather than by anyone's intent.

What an Undefined Token Does

The failure mode the contract exists to prevent is quiet. A declaration like

css
font-size: var(--font-size-s, 0.875rem);

where--font-size-sis defined nowhere does not break anything. It renders through the fallback, every time, on every site. That is worse than breaking: the page looks styled, so nothing prompts a fix, but the value is frozen. Retuning the token does nothing because there is no token. The dark theme cannot reach it. The fluid type scale does not apply to it. It is a hard-coded value wearing a token's clothes.

This is how components written against a vocabulary that never existed behave. A component that references--font-size-swhere the vocabulary spells it--font-s, or--text-colorwhere the vocabulary says--color-text, is not wrong-looking, just permanently deaf to the design system. The fix belongs in canon, and it is a mapping: replace each foreign reference with the real token it means, either directly or through a prefixed component property whose default resolves to it. The tempting fix, adding the foreign names to your site's vocabulary so the references resolve, is the wrong one. It works immediately and costs forever: the vocabulary then carries two naming schemes for the same concepts, and every future site inherits both.

Auditing for undefined tokens finds adjacent decay too. This library's audit turned up a component property that was defined and documented but consumed nowhere; the knob had quietly stopped doing anything across a refactor, and nothing visible ever hinted at it. Dead wiring and fallback-only wiring are the same disease: styling that looks intentional and answers to no one.

Auditing the Contract

Each side of the contract has its own audit.

In the library,npm run lint:componentslints every component against the vocabulary: token references that nothing defines, component properties missing their owner's prefix, stylesheets that would escape the cascade layer order, and manifest defaults that drifted from validation. A bare reference to a missing token is an error, because it renders unstyled. A reference with a fallback is a warning, because it renders and lies about it, which you now know is the more patient problem.

In a consuming site, the starter ships the mirror image asnpm run tokens:check. It audits your installed components against your vocabulary and reports in the same shape:

text
token contract: 31 installed components, 108 vocabulary tokens, 0 errors, 3 warnings

A clean report means every token your components consume is one your vocabulary actually defines, which is the contract, verified. When warnings do appear, read them before acting on them. A warning naming a component's own file usually means canon has already fixed it and your copy predates the fix;npm run components:statuswill show the component as outdated, and a reinstall clears the warning. A warning naming a token you renamed or removed from your vocabulary means the break is on your side, and the token needs to come back.

Runtokens:checkafter installing a component, after updating one, and after any change to_design-tokens.css. It is fast, and it is the difference between a design system you can retune from one file and a slow accumulation of values that merely look adjustable.