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.
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-hoverDeterminism 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.
In code, the round trip is mechanical in both directions:
// Forward: source path → CSS variable (the generator's job)
"motion.duration.extraLong1": { "$value": "1500ms" }
→ --core-motion-duration-extra-long1: 1500ms;
// Reverse: CSS variable → source (any engineer's job, from memory)
--semantic-color-action-background-primary-hover
→ semantic/color.tokens.json → action.background.primary.hover
// The lint floor both directions stand on
npm run tokens:lint
check-kebab-token-vars.mjs // emission stays kebab
check-token-references.mjs // references stay layer-legalApplied Example: Naming a New Token Without Regret
A new state arrives: cards need a "dragging" look. Name it correctly in five moves:
- 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.
- 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. - 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. - Reference legally: its
$valuepoints at{elevation.level.2}—semantic to core, the direction the reference linter requires. - 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-primary2. 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
Apply This Concept
A teammate proposes renaming foreground.secondary to text.dim because it is shorter. Walk the full cost: consumers, themes, tools, and the conditions under which the rename is nonetheless correct.
Reflect
You find --semantic-color-brand-blue-hover in a stylesheet. List every rule this name violates and reconstruct what its author probably needed.
