Learning Track
Filter content by your role to see the most relevant sections
Component Token Mapping
Map tokens to component anatomy so design decisions translate visibly to implementation: per-slot token contracts with resolution targets and fallbacks, scoped CSS emission, anatomy tables generated from the same source, and the discipline that keeps every part answerable.
Why This Matters
The most common question in any design system slack channel is a variant of "which token do I use here?" Teams that answer it with tribal knowledge get slower forever: every onboarding, every review, every incident re-asks it. The question becomes cheap exactly when the component itself carries the answer—when each anatomical part declares the tokens it consumes, in a form people and tooling can read.
That declaration is what a component contract is. In this repository, every component ships a <Name>.contract.json next to its code: its anatomy (slots and their selectors), its variants and states, its accessibility facts, and—central to this page— its per-slot token map: which token each part consumes, what it resolves to, which CSS property it feeds, and the fallback that keeps the part renderable. The documentation's anatomy tables are generated from these files; nothing is hand-synced.
The payoff is symmetrical. Designers see where each decision lands (this role, this part); engineers see what each part owes (this token, this fallback); and the system sees its own coverage—which parts consume which layers, and where a component is reaching past its contract.
Core Concepts: Slots, Bindings, Emission, Generation
Anatomy First: Parts Have Addresses
A component's anatomy is its named parts—slots—with a DOM address for each. The Icon contract is the smallest real example:
// ui/components/Icon/Icon.contract.json (excerpt)
{
"name": "Icon",
"layer": "primitive",
"anatomy": ["root"],
"slots": {
"root": { "required": true, "selector": "[data-slot=\"icon\"]" }
},
"a11y": { "role": "generic", "labeling": [], "keyboard": [] }
}Compound components have richer trees—Tabs declares root, list, tab, and panel slots, each with its selector—and the discipline is the same: if a part matters enough to style, it matters enough to name and address. The data-slot attribute is how the contract's selector and the rendered DOM stay honest with each other.
The Token Map: Bindings Per Slot
Inside each slot, the contract enumerates its token bindings—the actual mapping this page is named for:
// Icon.contract.json — tokens for the root slot
"tokens": {
"root": {
"icon.color.foreground.default": {
"resolvesTo": "semantic.color.foreground.primary",
"fallback": "#141414",
"property": "color",
"layer": "semantic"
},
"icon.size.padding.default": {
"resolvesTo": "core.spacing.size.01",
"fallback": "1px",
"property": "padding",
"layer": "core"
}
}
}Read one binding as a complete sentence: the icon's default foreground color is the semantic foreground-primary token, feeding the color property, with a #141414 fallback if the sheet is missing. Four fields, and every question a consumer or reviewer asks is answered: what it consumes (resolvesTo), what breaks first (fallback), where it lands (property), and which layer it lives in (layer)—so a component reaching for a core binding directly is visible in its own contract, not just in a lint run.
These bindings scale with anatomy: Alert's contract carries fifty, AlertNotice fifty-seven—every region, state, and adornment of those components is bound, which is why their theming behavior is predictable without reading their CSS.
Emission: The Scoped Wrap
The build turns bindings into the scoped CSS tier you have met throughout the foundations—a <Name>.tokens.css per component, scoped by [data-ds-component] and prefixed --ds-<name>-*:
/* Generated from Card.tokens.json — prefix + references */
[data-ds-component='Card'] {
--ds-card-color-background-default:
var(--semantic-color-background-primary, #ffffff);
--ds-card-color-background-hover:
var(--semantic-color-background-secondary);
--ds-card-color-foreground-link:
var(--semantic-color-foreground-link);
}The mapping thus has three synchronized representations: JSON contract (source), scoped CSS (runtime), and the component's own styles consuming only --ds-* names. One source of truth, two projections—and a mismatch between them is a build defect, not a documentation drift.
Generation: Docs From the Same Source
Because the map is data, the documentation is generated: the component-standards anatomy tables (AnatomyTable over generateAnatomy) read the same contracts and render parts, slots, and tokens as real table rows. A new binding appears in the docs the next time they build; a removed one disappears. Documentation that cannot drift is the quiet superpower of mapping-as-data.
System Roles: Who Maintains the Map
Design Impact
Designers read the map to see their decisions land: the border role they renamed shows up (or fails to) in the contracts that consume it. The map is also the review surface for coverage—"this state has no binding" is visible before it becomes "this state looks wrong in dark mode."
Engineering Impact
Engineers keep the three representations synchronized by keeping one source: bindings land in the contract, the build emits the CSS, and component styles never hardcode what a binding already carries. The contract file is reviewed like code because it is code.
Governance Impact
Governance reads the aggregate: which components bind to which layers, where core-layer bindings cluster (a smell worth a conversation), and whether new semantics are being adopted or bypassed. The map turns "is the system being used?" from a feeling into a query.
Design & Code Interplay
On the design side, the map appears as the component library's own structure: each component variant's layers named for the parts (matching slots), painted only with the variables the contract binds. A designer inspecting the library's Card sees background-default as a variable—because that is what the engineer's Card consumes. The shared vocabulary is not aspirational; it is generated from one file.
In code, adding a binding is the complete change—and its paper trail:
// 1. Bind in the contract (source of truth)
"tokens": { "root": {
"icon.elevation.dragging.default": {
"resolvesTo": "semantic.elevation.surface.dragging",
"fallback": "0px 3px 6px rgba(0, 0, 0, 0.14)",
"property": "box-shadow",
"layer": "semantic"
} } }
// 2. Build — the scoped tier appears
[data-ds-component='Icon'] {
--ds-icon-elevation-dragging-default:
var(--semantic-elevation-surface-dragging,
0px 3px 6px rgba(0, 0, 0, 0.14));
}
// 3. Consume — the component style uses only its own name
.iconDragging { box-shadow: var(--ds-icon-elevation-dragging-default); }
// 4. Docs — AnatomyTable renders the new row next build,
// and the reference lint verifies the resolution direction.Applied Example: Answering "Which Token Do I Use?"
A product engineer asks: "I'm building a status chip inside a Card—what color do I use?" Show the map answering:
- Find the nearest part: the chip is not a new anatomy problem—Badge's contract already binds status surfaces. Read its bindings:
badge.background.successresolves tosemantic.color.background.successSubtlewith anonSuccessSubtleforeground pair. - Prefer the component over the raw pair: the mapping says the system's decided answer to "status chip" is Badge—consuming the pair directly re-opens pairing decisions (which fg goes with which bg) the component already made.
- If it must be custom, bind it: a truly novel part gets a slot and a binding in its own contract—not a one-off hex. The next person with this question finds the answer in the same place you just did.
- Close the loop: the anatomy docs the engineer was reading were generated from those same contracts—the question was already answered; the map just had to be where the question was asked.
Constraints & Trade-offs
- Binding granularity vs contract weight: every binding is reviewable, generated, and documented—but fifty-binding contracts cost more to read than a CSS file. The count tracks real anatomy; the cure for bloat is simpler components, not sparser maps.
- Core-layer bindings: the schema permits binding core tokens directly (Icon's padding), and primitives are the honest case—general components should prefer semantic resolutions so themes can reach them.
- Generated docs vs curated docs: generation guarantees accuracy and surrenders narrative; the working split is generated structure with a thin authored layer on top, never the reverse.
- Fallback maintenance: each literal fallback is tracked drift risk; the same change must update token and fallback or the resilience story lies quietly.
Common Pitfalls & Failure Modes
1. Unmapped parts
// Bad: A styled region with no slot, no binding
.extraRow { background: #f5f5f5; }
// Good: Name it, bind it, and the question answers itself forever2. Contracts drifting from CSS
Hand-edited token CSS that the contract does not declare (or vice versa) makes the map lie. The build is the only writer of the scoped tier; the contract is the only source of the build.
3. Consuming past the map
/* Bad: The chip bypasses Badge's decided pairing */
.chip { color: var(--semantic-color-foreground-success); }
/* Good: Consume the component, or bind a proper pair */4. Bindings without fallbacks
A binding that omits its fallback removes the render-without-stylesheet guarantee and the runtime observability point in one omission.
Verification Checklist
Additional Resources
- Component anatomy standards — the tables generated from these contracts (
/blueprints/component-standards/anatomy) - Component architecture — the layering the map hangs on (
/blueprints/foundations/component-architecture) - Token naming — how bound names stay legible (
/blueprints/foundations/meta/token-naming) - The sources — any
ui/components/*/[Name].contract.json(start with Icon, then Alert for a fifty-binding example)
Related Concepts
Reflection Questions
Apply This Concept
Open any component you own and list its styled regions. Which ones have slots and bindings, and which are unmapped? Write the binding sentences for one unmapped region.
Reflect
The Icon contract binds core.spacing.size.01 directly. When is a core-layer binding the honest choice, and what does its visibility in the contract buy that a hidden hex never could?
