Skip to main content

Learning Track

Filter content by your role to see the most relevant sections

Token Naming & Hierarchy

Structure tokens for clarity, reuse, and scalability: layer namespaces, a deterministic path-to-variable transform, role-first semantic naming, and the conventions that let any consumer locate the decision they need without a guide.

Darian Rosebrook
Staff Design Technologist, Design Systems Architect
Expertise:Design Systems, Tokens, Naming

Why This Matters

Names are the interface. A token's value can be wrong and fixed in one place; a token's name is read a thousand times—by designers choosing, by engineers consuming, by grep at 2am during an incident, by the new hire learning where decisions live. Naming that must be explained verbally has already failed; the whole point of a token system is that structure carries the explanation.

Hierarchy is the other half. This system spans three layers and three themes, and its single defense against that complexity is that every name reveals its own position: which layer it lives in, what it decides, and how it got from JSON path to CSS variable. None of that is convention-by-memo; it is a deterministic transform enforced by linters, which means naming cannot drift even when attention does.

This page covers the naming system as built here: the DTCG source format, the namespace rules in the generator, the path-to-variable transform, and the semantic naming grammar.

Core Concepts: Format, Namespace, Transform, Grammar

The Source Format Is DTCG

Token sources follow the W3C Design Tokens format: groups nest objects, and the reserved $-prefixed keys carry the metadata—$value, $type, $description, $extensions. The discipline that matters for naming: every path segment is a decision. color.palette.blue.600 reads as domain→collection→family→step, and each segment answers a question someone will ask.

Namespace by Rule, Not by Memory

Which layer a token belongs to is not a judgment call; the generator classifies by path pattern (utils/designTokens/generators/generateCSSTokens.mjs):

// determineNamespace(tokenPath) — core patterns (verified excerpt)
color.(mode|palette|datavis).*      typography.(fontFamily|weight|ramp|…)
spacing.size.*                      elevation.(level|offset|blur|spread).*
opacity.(50…full)                   dimension.(breakpoint|tapTargetMin|…)
shape.(radius|border.*)             motion.(duration|easing|keyframes|delay|stagger)
scale.*  density.*  layer.*  layout.*  icon.*  effect.*

// Everything else — foreground, background, border, action, feedback,
// status, syntax, control, interaction… — is semantic.

The rule encodes the layer doctrine: primitives and raw scales are core; anything that names a job is semantic. Because it is a rule, the prefix on a CSS variable is trustworthy evidence—seeing --core- in component code means a layer boundary was crossed, which is exactly what the reference linter flags.

The Transform Is Deterministic

From JSON path to CSS custom property, four mechanical steps, no exceptions:

// tokenPathToCSSVar — dots → hyphens, camelCase → kebab,
// strip invalids, collapse hyphens, prepend namespace

color.palette.blue.600      → --core-color-palette-blue-600
motion.duration.extraLong1  → --core-motion-duration-extra-long1
effect.backdropBlur.sm      → --core-effect-backdrop-blur-sm
foreground.primary          → --semantic-color-foreground-primary
action.background.primary.hover
                            → --semantic-color-action-background-primary-hover

Determinism is the feature. Anyone holding a token's source path can write its CSS name and be right; anyone reading a CSS name can walk back to the source file without a lookup table. Systems where that round-trip needs a human acquire a translation layer, and translation layers acquire bugs.

Semantic Grammar: Role, Property, State

Semantic names follow one grammar: role → property → state, deepest last: action.background.primary.hover — the action role, its background property, the primary variant, hover state. The test is the sentence test: "the hover background of the primary action" reads correctly in reverse. Names that fail it— blueDark2, color_new, btnBg—encode implementation order instead of intent, and intent is what survives refactors.

The component layer adds one wrap: --ds-<component>-<property>, scoped by [data-ds-component]—so --ds-card-color-background-default is unambiguously the Card's opinion, resolvable to a semantic token with a literal fallback. Three tiers, one grammar, zero ambiguity about who owns a name.

What the Linters Enforce

Two scripts make the conventions load-bearing: scripts/check-kebab-token-vars.mjs fails any emitted variable that is not kebab-case (the transform's contract), and scripts/check-token-references.mjs walks token references for the layer rules—core references core, semantics reference core, components reference semantics. Naming that is linted does not need memorizing; it needs only to stay deterministic.

System Roles: Who Names What

Design Impact

Designers own semantic grammar—the role words, the state vocabulary, the decision of when a variant deserves a name. The Figma variable tree mirrors the JSON paths, so a designer's naming choice is the engineer's consumption contract from day one.

Engineering Impact

Engineers own the transform and its invariants: namespace correctness, kebab emission, reference direction. When a new token group is added, the pattern list in the generator is the only place to register its layer—and the only place it can be gotten wrong centrally instead of everywhere.

Governance Impact

Governance owns renames, which are API breaks: a semantic name with consumers is a contract, and changing it is a deprecation cycle, not a find-and-replace. The naming hierarchy makes the blast radius of any rename visible before it starts—which names are internal (core steps) and which are public (semantic roles, component tokens).

Design & Code Interplay

In the design tool, the hierarchy appears as the variable tree's shape: collections for domains, groups for families, variables named by the same grammar. A designer creating a semantic variable must finish the sentence "this is the … of the …" to name it at all—the tree enforces role-first thinking the way the JSON does.

The review ritual that keeps the two sides identical: pick any variable in the file, read its path aloud, and confirm the CSS name derives by the transform. Ten seconds, any token, and it catches drift the moment it appears rather than the quarter after.

Applied Example: Naming a New Token Without Regret

A new state arrives: cards need a "dragging" look. Name it correctly in five moves:

  1. Layer first: is this a primitive (new elevation level? new shadow?) or a role (existing parts, new intent)? Dragging typically composes existing elevation and opacity—the answer is semantic, and the name starts in the semantic file.
  2. Finish the sentence: "the elevation applied while the card is dragged" → elevation.surface.dragging—role (surface), state (dragging). If the sentence needs the word "blue" or "8px", stop: that is a value leaking into a name.
  3. Check the transform: the emitted name will be --semantic-elevation-surface-dragging—run the sentence test on that, since it is what a thousand future readers will see.
  4. Reference legally: its $value points at {elevation.level.2}—semantic to core, the direction the reference linter requires.
  5. Register consumers: the Card's scoped token wraps it (--ds-card-elevation-dragging) so the name gains a component surface without becoming one.

Five moves, zero new conventions, and the name is now findable by role from either side of the design/code line.

Constraints & Trade-offs

  • Path-length vs flatness: deep paths (action.background.primary.hover) carry structure but lengthen names and files; flat short names are punchy and lose the taxonomy. The system takes depth at the semantic layer because roles are the taxonomy.
  • Namespace-by-regex vs explicit layer files: classifying by pattern keeps sources free of boilerplate but centralizes the truth in one regex list—a new primitive family that forgets to register silently lands in semantic. The lint catches direction, not misplacement.
  • State vocabulary size: every state word (default/hover/active/disabled/dragging…) multiplies names. The working rule from the color page applies verbatim: add a state when it themes or fails differently, not because a variant exists.
  • Stable names vs better names: renaming for taste breaks every consumer; the discipline is to live with a mediocre-but-stable name and reserve renames for genuine semantic errors, executed as deprecations.

Common Pitfalls & Failure Modes

1. Values in names

// Bad: The name expires when the value changes
"spacing.size.16px"  /  --color-blue-600-text

// Good: Name the decision; let the value be data
"spacing.size.06"    /  --semantic-color-foreground-primary

2. Implementation names for semantic roles

// Bad: Says where it renders, not what it decides
"sidebarAccentBevel"

// Good: Role → property → state
"elevation.surface.dragging"

3. Synonym drift

text/foreground/label for the same role; gap/stack/spacing for the same property. Every synonym doubles the search space and halves the trust. One glossary, enforced in review, is the fix.

4. Skipping the component tier

/* Bad: Global semantics consumed raw by components */
.card { box-shadow: var(--semantic-elevation-surface-dragging); }
/* Good: The component names its own surface */
.card { box-shadow: var(--ds-card-elevation-dragging); }

5. Hand-written variable names

A CSS variable typed from memory that the transform wouldn't produce (wrong hyphen, kept camelCase) silently resolves to nothing—or worse, to a fallback. The kebab lint plus copy-from-source habit closes it.

Verification Checklist

Additional Resources

  • Atomic vs Semantic — the layer doctrine these names encode (/blueprints/foundations/meta/atomic-vs-semantic)
  • Core vs Semantic deep-dive — the worked layer boundary (/blueprints/foundations/tokens/core-vs-semantic)
  • DTCG format — the source syntax (https://tr.designtokens.org/format/)
  • The sources — utils/designTokens/generators/generateCSSTokens.mjs (the transform), scripts/check-*-token-*.mjs (the lints)

Related Concepts

Reflection Questions