Brik Design System
Foundation

Token Anatomy

The six disambiguated concepts that describe every BDS token — Anatomy, Tier, Library, Layer, Mode, Tenet — and the four-tier abstraction stack every token belongs to.

Every BDS token answers six independent questions. Without distinct vocabulary for each, agents and humans invent parallel taxonomies and the system drifts (portal #512 / #553, retired --theme-brik-*, retired --brand-{tier}). This page locks the vocabulary so the discipline lives in the words themselves.

Six concepts, six words

ConceptTermQuestion it answersValues
Naming structureAnatomyHow is it named?--{purpose}-{role}[-{state}]
Abstraction levelTierWhat kind of token is it?Raw · Primitive · Semantic · Component
Source of truthLibraryWhere is it defined?Foundations Library · [Client] Brand Kit Library
CSS cascade originLayerWhich @layer carries its value?bds-tokens · bds-components · client-theme · client-overrides
Value axisModeWhich axis varies its value?color light/dark · borderwidth thin/bold · spacing compact/spacious / etc.
System pillarTenetWhich system pillar governs it?Foundation · Theming · Motion · Content

These are orthogonal. A token has one answer for each. Mixing them — calling --brand-tertiary a "tier" because it sits at the top level — is the failure mode the cleanup retired.

Before referencing a token name, you should be able to answer all six. If you can't, you're guessing — verify against the canonical registry (dist/tokens.css) first.

Anatomy

Color tokens follow one of two parallel formulas — an intent form and a scoped-intent form. Everything that is not a color follows a third: see Non-color anatomy.

Intent:         --{purpose}-{role}[-{modifier}][-{state}]
Scoped intent:  --{purpose}-{scope}-{scope-value}[-{modifier}][-{state}]

Each segment is fixed vocabulary:

SegmentAllowed valuesExample
purposepage, surface, background, text, border, color (primitives only)--background-…
roleClosed list — see Color foundations vocabulary--background-brand-primary
scope / scope-valueA named context that carries its own hue — service-{line}, presence-{state}--background-service-marketing
modifier (optional)Context / tone / hierarchy — see Color modifiers--background-service-brand-on-light
state (optional)hover, pressed, disabled--background-brand-primary-hover

Status, service-line, and presence semantic tokens use the scoped-intent form — the scope (service-marketing, presence-online) already names the hue, so they never also take a -brand/-primary role segment. Everything else uses the plain intent form.

Token names without a purpose prefix are drift. Examples retired by brik-bds#712: --brand-primary (missing purpose; legitimate form is --{purpose}-brand-primary), --theme-brik-* (entire namespace violates the anatomy). If you see something shaped --brand-X or --theme-X-Y in old code, it's drift and should not be referenced.

When to add a hierarchy modifier

Never preemptively. A role starts as a single-level token set — one token, no hierarchy modifier (--text-service-marketing, which ships with no -primary / -secondary sibling). Add a -primary / -secondary pair only when the design carries a genuine second emphasis level within that role, distinct from the generic --text-secondary / --text-muted that already exist for de-emphasis.

/* ✅ single-level — one emphasis, no hierarchy modifier (real token) */
--text-service-marketing

/* ❌ preemptive — no --text-service-marketing-secondary exists to pair it,
   so the -primary suffix is just a longer name for the single-level token */
--text-service-marketing-primary

The pair earns its place only when a second emphasis tier actually ships alongside the first; until then, --text-secondary / --text-muted already cover generic de-emphasis. Scoped tokens never stack a hierarchy modifier under a context modifier either — the (text, surface) pairing carries emphasis implicitly, so --text-service-brand-on-dark is complete without a -primary.

Non-color anatomy

Half the registry is not a color. 325 of the 701 token names in dist/tokens.css sit outside the color purpose list above — --font-size-100, --gap-md, --ease-spring. They are a second legitimate formula, not drift. The distinction the color form draws between purpose and role does not apply, because there is no hue to place: a length, a duration, or a weight is fully described by the property it feeds and the step on that property's scale.

Primitive:  --{property}-{step}      --font-size-100, --border-radius-400
Semantic:   --{role}-{step}          --gap-md, --heading-xl, --ease-spring

The first slot is a CSS property name at the Primitive tier and a semantic role at the Semantic tier. Both may be hyphenated (font-line-height, content-width) — the slot is the whole property or role name, not the first segment.

Primitive tier property scales

SlotStep vocabularyFeeds
--font-size-*numeric (100 = 16px)font-size
--font-weight-*word (semibold)font-weight
--font-family-*role (body, heading)font-family
--font-line-height-*word (normal, snug)line-height
--font-casing-*word (uppercase)text-transform
--letter-spacing-*word (wide)letter-spacing
--space-*numeric (400 = 16px)any spacing property
--size-*numeric + pill / circlewidth / height
--border-radius-*numeric + t-shirtborder-radius
--border-width-*numeric + t-shirtborder-width
--shadow-blur-* · --shadow-offset-* · --shadow-spread-*numericthe parts of a box-shadow
--blur-radius-*t-shirtfilter: blur()
--duration-* · --delay-* · --iteration-*numerictransition / animation
--breakpoint-*word (desktop)media queries
--aspect-*ratio (16-9)aspect-ratio

Semantic tier roles

SlotStep vocabularyWhat it names
--gap-* · --padding-* · --gutter-*t-shirtspacing by intent, not by pixel step
--heading-* · --display-* · --body-* · --label-* · --subtitle-*t-shirtthe type ramp per text role
--icon-*t-shirticon sizes, aligned to the type ramp
--shadow-* · --box-shadow-*t-shirtelevation
--ease-*word (spring)easing curves
--content-width-* · --measure-*t-shirtlayout and line-length ceilings
--state-*state (hover, disabled)interaction overlays and opacities

The Component tier is unchanged — --bds-{component}-{property}, per ADR-014.

Two first segments carry both formulas. --border-* is 20 color tokens and 38 length tokens (--border-radius-*, --border-width-*); --shadow-* is elevation and the primitive parts (--shadow-blur-*, --shadow-offset-*, --shadow-spread-*). Match on the longest slot, never the first segment — --border-radius-100 is a length, not a color.

Named exceptions

Two kinds of thing live outside the tables above. Both are in the registry today; neither should be referenced in new work.

Renames owed — tracked on #1910, sequenced behind the naming ADR.

Shipping nameWhy it's driftUse instead
--tooltip-background, --tooltip-textComponent tier without the mandatory --bds- prefix — the retired --{component}-{prop} pattern.--bds-tooltip-*
--easing-ease-in, --easing-ease-out, --easing-ease-in-outStyle Dictionary's primitive export. A second name for one concept, at different curve values, and the easing-ease- stutter is the tell. Zero references in components/; --ease-* carries all 70.--ease-in / --ease-out / --ease-in-out

Slotless by exception--web, --tablet, --mobile (tokens/figma-tokens.css:319-321) carry no purpose slot and are a deliberate carve-out, not a rename target: they are unitless Figma primitives, and the --breakpoint-* family that would receive a rename has no consumers either. Measured on 2026-08-20, both families are unconsumed — zero var() references to --web/--tablet/--mobile or --breakpoint-* anywhere in brik-bds, brik-client-portal, or brikdesigns. Whether BDS should ship breakpoint tokens at all is an open question for the naming ADR; until it answers, renaming one dead family into another buys nothing. Mirrored in EXCEPTIONS at scripts/__tests__/inspect-widget-tokens.test.mjs.

Step vocabularies are not settled here. Nine slots draw from more than one — --border-width-* carries numeric (100), t-shirt (md), and thin/standard/bold; --duration-* carries both 200 and fast. A second kind of split the census does not flag, because both halves are t-shirt: --icon-* spells the same step tiny in one place and 2xs in another. Which scale is canonical per family is #1910 axis 5, decided in the naming ADR. Run npm run lint-token-purpose-slots -- --census for the current measurement.

This section is gated: npm run lint-token-purpose-slots fails when a slot ships in dist/tokens.css that is neither named above nor dispositioned as drift. That is the only thing keeping the tables from going stale — a color-only formula describing half a system is exactly how #1910 started.

Color modifiers

Within the color role/modifier vocabulary, four concepts share light/dark wording but answer different questions. Pick the wrong one and it compiles — silently wrong.

on-color

The legible foreground for content resting on a colored, branded, or dark surface. Grayscale, brand-agnostic, consumed by component variants (Button on-color, the Tab-bar onColor mode).

--text-on-color-light   /* dark FG  — for a light colored fill */
--text-on-color-dark    /* white FG — for a dark colored fill  */

on-color is the whole role; light/dark names the fill tone it targets. It never carries a hue.

-on-light and -on-dark

For a token whose hue is already named (a service line, a brand scope), pins the value for a known, fixed backdrop tone.

--background-service-brand-on-light   /* brand fill, on a light surface */
--text-service-brand-on-dark          /* brand text, on a dark surface  */

The -color- infix is dropped because the scope (service-brand) already implies color. The leading on- marks context — distinct from tone.

Two on forms, on purpose. on-color-* is a complete neutral role; -on-light/-on-dark is a context modifier on a colored scope. Same word, different slot in the formula — not an inconsistency, and not a rename candidate.

-inverse

The hard reverse across light/dark mode (lightest ↔ darkest), mirroring the reverse of a -primary token. A theme-level concept — --background-inverse, --text-inverse. A service line normally adapts to a known backdrop through its -on-light/-on-dark context tokens, not a per-line inverse — with one carve-out: --surface-service-{line}-inverse (surface purpose only) flips white in light → {hue}-darkest in dark, for service-identified cards that must read as neutral chrome in light mode but carry the line's deep tint in dark. See ADR-020. No per-line inverse exists on background/text/border.

-light and -dark

The pastel or deep shade of a surface (--surface-service-brand-light = pastel). Tone shifts the shade; context targets the backdrop. Tone is why scoped context had to be spelled -on-light/-on-dark-light/-dark was already taken.

You want…Token
Legible text/icon on any colored or dark fill--{purpose}-on-color-{light|dark}
A service hue tuned for a known backdrop--{purpose}-service-{line}-on-{light|dark}
An element that hard-flips with the theme--{purpose}-inverse
A pastel/deep variant of a service surface--surface-service-{line}-{light|dark}
A service card surface: neutral white in light, deep tint in dark--surface-service-{line}-inverse

See Color for the full registry.

Tier

Every token sits at exactly one Tier. Higher tiers reference lower tiers via var(). Components only consume the upper tiers.

┌─────────────────────────────────────────────────────────┐
│  Tier 4 — Component    (component-scoped knobs)         │
│  --bds-{component}-{property}: var(<Semantic>)          │
│  Example: --bds-toast-shadow: var(--shadow-md)          │
└─────────────────────────────────────────────────────────┘

                          │ var()
┌─────────────────────────────────────────────────────────┐
│  Tier 3 — Semantic     (what components consume)        │
│  --{purpose}-{role}: var(--color-{family}-{step})       │
│  Example: --background-brand-primary:                   │
│             var(--color-poppy-500)                      │
└─────────────────────────────────────────────────────────┘

                          │ var()
┌─────────────────────────────────────────────────────────┐
│  Tier 2 — Primitive    (named raw values)               │
│  --color-{family}-{step}: #hex                          │
│  Example: --color-poppy-500: #e35335                    │
└─────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────┐
│  Tier 1 — Raw          (literal hex / numeric value)    │
│  Not a CSS variable — the literal value before naming.  │
│  Example: #e35335                                       │
└─────────────────────────────────────────────────────────┘

What lives at each Tier

TierNamingSourceComponents reference it?
RawLiteral #e35335Figma swatch / hex chosen by designNo — never in component CSS
Primitive--color-{family}-{step} (color), --font-size-{step} (type), --space-{step} (spacing), etc.Auto-generated from Figma Variables → Style DictionaryNo — never in component CSS (use Semantic)
Semantic--{purpose}-{role} — Color foundations vocabularyAuto-generated; client themes override values with same namesYes — this is the contract
Component--bds-{component}-{property}Authored in component / blueprint CSS; runtime values set by the component's JS/TSXYes — but only as a knob: it must resolve to a Semantic token (see below)

Components consume Semantic tokens, never Primitives directly. A button's CSS references --background-brand-primary, not --color-poppy-500. Why: Semantic is the layer that varies per brand. Bypassing it locks the component to one brand.

About Tier 4

The Component tier is --bds-{component}-{property} — a component- or blueprint-scoped custom property, where {component} is the component/blueprint identifier and {property} is the styled aspect. Examples: --bds-toast-shadow, --bds-slider-thumb-shadow, --bds-hero-img-card-bg. This is the only sanctioned Component-tier namespace (ADR-014).

A Tier 4 token serves one of two roles, both under the same name:

  1. Override knob — a hook a component reads so a consumer can re-skin one instance without forking the CSS. It must resolve to a Semantic token, never a raw value:

    /* ✅ knob resolves to a Semantic token */
    box-shadow: var(--bds-toast-shadow, var(--shadow-md));
    
    /* ❌ raw literal in the fallback — reintroduces an off-token value
       that ships silently if the token fails to resolve, and lint can't see it */
    box-shadow: var(--bds-toast-shadow, 0 4px 12px rgba(0, 0, 0, 0.08));
  2. Runtime binding — a value the component's JS/TSX sets at render (e.g. --bds-slider-percent), read by the CSS to position or size an element.

Tier 4 never terminates at a raw value. A knob's fallback is a Semantic token; a runtime binding is computed from one. A literal inside a var() fallback on a tokenized property is Tier-1 leakage and is flagged by scripts/lint-tokens.js. The --bds- prefix is mandatory — --bp-* and bare --{component}-* are retired (see Drift patterns below).

How the six concepts map to a real token

Take --background-brand-primary (the hot path):

ConceptAnswer
Anatomy--{purpose: background}-{role: brand-primary}
TierSemantic
LibraryDefined in Foundations Library as the canonical name; the value is overridden by each Brand Kit Library
Layerbds-tokens for the canonical default; client-theme for the per-client override
ModeVaries by color mode (light / dark)
TenetFoundation (token canon) + Theming (brand override)

Every legitimate token answers all six. If a token doesn't fit any one of them, it's drift.

Drift patterns to avoid

Drift patternWhy it's wrongRetired in
--theme-{name}-* (--theme-brik-green, --theme-blue-blue-light)Doesn't match Anatomy — no purpose prefix. Duplicate alias of a --color-{family}-{step} primitive.brik-bds#712 / #727
--brand-{tier} (--brand-primary, --brand-tertiary)Brand-tier names without purpose prefix. Legitimate composed form is --{purpose}-brand-{tier}.brik-bds#712 / #727
--surface-paper, --text-on-ink, --surface-soft, --surface-warmInvented modifiers or invented role values that aren't in the closed Color foundations vocabulary. Always parallel taxonomy.portal #512 / #553 (rolled back)
Component CSS reading --color-{family}-{step} directlyBypasses the Semantic tier. Locks the component to one brand.Caught by scripts/lint-tokens.js
--bp-{blueprint}-{slot}-{prop} (--bp-hero-img-card-bg)Parallel Component-tier namespace that shipped ahead of canon. Invisible to lint because it's an undefined reference, not a real token.#1043 / ADR-014 → migrate to --bds-*
--{component}-{prop} (--toast-shadow, --select-chevron-color)Component knob without the --bds- prefix. Collides with the global namespace and isn't lint-recognized.#1043 / ADR-014 → migrate to --bds-*
Raw literal inside a var() fallback on a tokenized property (var(--shadow-md, 0 4px 12px …))Reintroduces a Tier-1 value that ships silently if the token fails to resolve; lint sees only the canonical name and passes.Caught by scripts/lint-tokens.js (fallback-literal rule, #1043)

Inventing parallel taxonomy is the failure mode this vocabulary exists to prevent. Three rollbacks (portal #512, portal #553, brik-bds#712 cleanup) trace to agents inventing names that almost fit. When in doubt, verify in dist/tokens.css — if the name isn't there, it isn't real.

Verifying a token before referencing it

# Is this name in the canonical registry?
rg '\-\-{token-name}' node_modules/@brikdesigns/bds/dist/tokens.css

# Or run the consumer-side gate
./scripts/token-audit.sh --canonical-only

If the gate is missing or the name isn't found, stop. Don't ship a var() call against a name that doesn't exist.

  • The Cascade — Layer ordering, modes, color foundations vocabulary
  • Theming — how a client brand overrides these Semantic tokens per the four Theming Dimensions; the "How Content feeds Theming" section separates theming layers from content vocabularies
  • Composition Layers — the pedagogical Section → Layout → Container → Block or Control → Component model that composes tokenized primitives
  • Figma Library architecture — Foundations Library vs Brand Kit Library
  • Color — full canonical color registry
  • Color Pairings — the accessible foreground/background pairing system the on-color and -on-light/-on-dark modifiers feed into, with the contrast matrix and CI gate
  • Color primitive tiers — the closed tier vocabulary used by the portal theme generator
  • Slot Vocabulary — BEM slot vocabulary (different domain — component class names, not token names)
  • Style Dictionary build pipeline — how Figma → Library → Layer → CSS happens

On this page

💬