Design System Contracts

How it works · foundations 3

How styles are applied

A contract never contains a color. It contains token names, and each surface has its own moment for turning a name into pixels — CSS rules baked at generate time with values bound at runtime on the code side; native variables bound through the Plugin API on the canvas. Same names, two dialects. Every artifact below is loaded from the repository at build time; the worked example is Badge's background.

Names, not values

Badge's contract (ds.badge v1.1.0) binds its root part to token references — including a {variant} placeholder that expands per enum value of the variant prop:

contracts/badge.contract.json (v1.1.0) — anatomy.root.tokens, loaded at build time
{
  "anatomy": {
    "root": {
      "tokens": {
        "background-color": "{color.feedback.{variant}.background}",
        "color": "{color.feedback.{variant}.foreground}",
        "padding-inline": "{space.inset-x.sm}",
        "padding-block": "{space.inset-y.sm}",
        "border-radius": "{radius.badge}",
        "font-family": "{font.control.family}",
        "font-weight": "{font.control.weight}",
        "font-size": "{font.badge.size}"
      }
    }
  }
}

No hex codes, no pixel values. The integrity gate guarantees every expanded reference resolves to a real token — {color.feedback.success.background} must exist in tokens/ or the build fails by name. What the tokens are worth is the token layer's business, and that separation is the entire trick.

The code surface: two stages

Stage 1 — rules, baked at generate time

The generator compiles the anatomy to a CSS Module: one class per variant value, states as pseudo-classes. The React component never computes a style — it only selects classes:

src/components/Badge/Badge.module.css — generated from the contract; loaded at build time. The {variant} placeholder became .variant-info, .variant-success, … — one class per enum value.
.root {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 0;
  padding-inline: var(--space-inset-x-sm);
  padding-block: var(--space-inset-y-sm);
  border-radius: var(--radius-badge);
  font-family: var(--font-control-family);
  font-weight: var(--font-control-weight);
  font-size: var(--font-badge-size);
}

.variant-success {
  background-color: var(--color-feedback-success-background);
  color: var(--color-feedback-success-foreground);
}

Stage 2 — values, bound at runtime

Notice the generated rules still contain no colors — they reference var(--…) custom properties. The values arrive at runtime, from the token pipeline's own CSS:

the same custom property, defined per mode — loaded at build time from the compiled token CSS
/* src/styles/tokens.css (light) */
--color-feedback-success-background: var(--color-green-100);

/* src/styles/tokens.dark.css */
--color-feedback-success-background: var(--color-green-900);

This is why themes and modes work without regeneration: switching to dark mode swaps which custom-property definitions apply; every generated component re-resolves instantly, untouched. And it is why a token value change (the designer retargets color.feedback.success.background) regenerates only the token CSS — the component CSS references names, and the names didn't change.

The canvas surface: the same names, bound

The token pipeline that emits the CSS custom properties also syncs the same tree as native design-tool variables — collections carrying the pipeline's identity markers, upserted in place. The canvas emitter then compiles the contract to a sync script whose fills are bound to those variables through the Plugin API — never painted as literal colors:

figma-sync/05-badge.js (generated; excerpt loaded at build time) — the Variant=Success spec names the variable, not a color
"name": "Variant=Success",
        "row": 1,
        "col": 0,
        "spec": {
          "type": "root",
          "name": "Variant=Success",
          "layout": {
            "mode": "HORIZONTAL",
            "primary": "CENTER",
            "counter": "CENTER"
          },
          "fill": "color/feedback/success/background",
the binding call — figma.variables.setBoundVariableForPaint: the fill points at the variable; the design tool resolves the value per mode
return figma.variables.setBoundVariableForPaint({ type: 'SOLID', color: base, opacity: alpha }, 'color', v);

Variant substitution happens at compile time, exactly mirroring the CSS side: where the code surface got .variant-success, the canvas gets a Variant=Success component whose fill is bound to color/feedback/success/background. One placeholder in the contract, expanded once per surface dialect.

One statement, two dialects

background-color: var(--color-feedback-success-background) in a stylesheet and a Figma fill bound to color/feedback/success/background are the same statement — "this part's background is whatever that token says" — spelled in two dialects. Because both surfaces defer to one token source, mode switching behaves identically on both: neither surface owns a color; both re-resolve. And this is what makes drift checkable: the differ compares name bindings, not rendered colors — a mechanical, exact comparison. Pixels are the downstream receipt, verified separately by the visual-parity instrument against the design tool's own renders.

The honest asterisk: minted tokens

All of the above assumes the style facts arrive as names. Imports don't always oblige: a captured component may carry a literal #16a34a nowhere in any token set. The pipeline never invents a name and never silently drops the fact — it mints a provisional token under the imported.* namespace, binds to it, and marks it as minted in the proposal notes (the rename-later workflow: promote imported.color.3 to a real semantic name when a human decides what it means). The plugin route does better: a full-fidelity dump carries the real variable names the designer already bound, so captured names bind directly and minting is reserved for values that were truly raw. Either way the receipt is explicit — a minted token is a named question, not a papered-over answer.

Standing receipts: the token integrity gate fails the build on any unresolvable reference; brand-added-token-layer-only proves a new brand leaves every component byte-identical (values changed, names didn't); the census counts minted tokens per imported set; and the visual-parity instrument keeps the pixel receipt honest.