Token Architecture

Tokens are this system's real API — every color, size, radius and type style flows from one DTCG source file through three tiers into every component and into Figma. This page is the reasoning: how names are built, what each role owns, and the rules that keep five hundred tokens coherent. If you ever extend the system, this is the contract you're extending.


Three tiers, one direction

Every color reference flows downhill: L1 primitives hold raw values (the palette ramps, the size scale) and are never bound by components directly for color; L2 semantic names roles on surfaces (bg-default, fg-muted, border-focus) and is the default binding for components; L3 conceptual exists only for the three concept families — action-*, field-*, surface-* — where components share a concept, not just a color. L2 and L3 hold only references, never literals. The one sanctioned flat scale: spacing and sizing bind L1 size-* directly.

L1{violet.600} → L2bg.accent → L3action.bg-primary-default → consumer.lb-btn--primary

The source is standard DTCG (tokens/source-tokens.json: $value/$type, aliases as {path.to.token}). scripts/build-tokens.js generates tokens/theme.css — generated, never hand-edited.

The naming grammar

One grammar for every color variable: prefix, role axis, role, then optional modifier and state. Reading a token name tells you what it paints and where it may appear.

--lb-namespace
action-L3 concept
bg-role axis
primary-variant
hoverstate (L3 only)

Two grammar laws: state suffixes live only at L3 (L2 is static vocabulary — there is no bg-accent-hover), and the modifier vocabulary is closed: intensity is subtle → muted → default → strong → bolder, roles are words like accent, inverse, focus, value, disabled. A new word costs the whole system a vocabulary entry — it must earn it.

Role prefixes — what each axis owns

The prefix names what the pixel is, not which CSS property draws it. bg-* owns surfaces and fills: containers, control fills, tracks, value-display fills. fg-* owns foreground ink: text and icons — every icon in the set draws with currentColor, so an outlined icon's SVG stroke is colored by color, an fg concern; the system's stroke-* tokens are widths, and no color-valued stroke token exists. border-* owns boundaries: component borders, dividers, outlines, focus rings — never icon artwork, never fills. Beside these sit surface-* and field-* (L3 concepts), and data-1..8 — the categorical chart palette, deliberately role-free.

  • action.bg-primary-default fills the button — bg axis, an area
  • action.fg-primary-default inks the label and the check glyph — one fg token, text and icon together
  • action.border-primary-default draws the boundary — sanctioned to equal the fill on filled controls
  • the ghost icon button's gear is pure fg: no fill, no border token touches the glyph

Property ≠ role. SVG stroke and CSS border-color may legitimately carry a bg-* token when they draw a graphic surface. The spinner's ring is drawn with borders; the donut's track is drawn with stroke — both are surfaces, so both bind bg roles. A chart axis drawn with stroke is a rule, so it binds border-muted. The role follows the pixel's meaning.

  • border-color: var(--lb-bg-bolder) — the ring's track is a surface, drawn via border
  • border-top-color: var(--lb-bg-accent-value) — the moving arc is a value display
  • no border-* token appears: nothing here is a boundary

Roles vs intensity — why bg.accent-value exists

Suffixes usually mean intensity. But some suffixed tokens are roles: fg.inverse-strong is the theme-stable on-color ink, and the *-value family are value-display fills. The test is the reference surface: the accent interaction chain (accent/-strong/-bolder) is tuned for white text on the fill at 4.5:1, so it darkens through states in both themes. A slider fill answers to a different surface — its neutral track, at 3:1 — so in dark mode it must go lighter, not darker. Different reference surface, different role. Every pairing is measured in tokens/CONTRAST.md.

  • bg.accent-value — the fill and thumb: violet-600 light, violet-400 dark (measured 6.4:1 on its track)
  • border-muted / bg-bolder — the tracks: neutral surfaces the value reads against
  • status progress bars use bg.success-value / -warning-value / -danger-value — same role, status hues

Cross-role rules — and the sanctioned exceptions

Binding one role's token where another role's pixel lives is forbidden — fg as a fill and bg as ink are the classic drifts, and audits hunt them. Exactly three patterns are sanctioned, each because the target's value is the source's surface:

Sanctioned pattern Example Why it's legal
Filled control's border = its fill action.border-primary-* → {bg.accent*} The border's value is the fill surface
Cutout ring = the surface it cuts into surface.border-cutout → {bg.default} The ring exists to be the page surface
Categorical palette is role-free data.1..8 as fill, stroke, or bg Categorical identity, not role-on-surface — by design

Minting a token — the checklist

Want to extend the system? Every new token answers five questions, in order:

  1. Is it really new? Grep the vocabulary first — most needs are an existing role or an existing intensity step.
  2. Which tier? A raw value is L1. A role on a surface is L2. Only action/field/surface concepts are L3 — and never as 1:1 aliases of an L2 token.
  3. Which existing word? The modifier vocabulary is closed; a genuinely new word needs the same scrutiny this page gives value.
  4. Measure it. Add its pairings to scripts/audit-contrast.js and keep CONTRAST.md green — the audit exits non-zero on failure.
  5. Hand it off. Token names are API: new tokens go to the Figma plugin via the handoff protocol so code and design never drift.

The Figma round-trip — the letbe plugin

The same source file drives both worlds. source-tokens.json generates the CSS and, through the letbe Figma plugin, becomes Figma variables — every alias intact, both themes, typography modes included. Design follows code: a token renamed in source is a breaking change everywhere at once, which is exactly the point. The round-trip runs both ways — the gallery's theme editor exports the same DTCG JSON the plugin imports, so a brand explored in the browser (or one of the built-in presets) becomes a Figma variable set in one import.

source-tokens.json → build-tokens.js → theme.css → every component
source-tokens.json → letbe plugin → Figma variables ⇄ theme editor export

Where to go next

Measured contrast Colors About letbe-ds llms.txt