all notes

Design tokens as code: the architecture I keep coming back to

Token taxonomy, naming, theming, and the boring middleware that makes Figma variables and CSS custom properties stay in sync. Six years of patterns I have stopped questioning.

Design tokens are the lowest layer of a design system. They are also the layer most teams get wrong. I have built and rescued enough token systems by now that I have a default architecture I reach for first. It works for solo projects, for product teams, and for enterprise platforms. It is boring on purpose.

Three layers of tokens

Primitive tokens are the raw values. color.gray.50 through color.gray.950. space.0 through space.16. font.size.xs through font.size.5xl. They are named by what they are, not what they do. Designers and engineers should rarely reach for primitives directly in component code.

Semantic tokens are the meaning layer. surface.canvas, surface.raised, surface.muted. text.primary, text.secondary, text.subtle, text.muted, text.placeholder. border.subtle, border.default, border.strong. These are named by their purpose. Components consume semantic tokens, not primitives.

Component tokens are the customization layer for individual components. button.primary.background, button.primary.text. card.surface, card.shadow. They reference semantic tokens by default, but can be overridden in specific contexts.

Why three layers

Primitive without semantic means components hardcode raw values, and theming becomes find-and-replace through every component every time the brand evolves. Semantic without primitive means you have no scale to ground the semantic decisions, and your spacing and color values drift. Component without semantic means you have a thousand component-specific tokens with no reuse, and your token file becomes unmanageable.

The three-layer model lets primitives be the source of truth for raw values, semantic tokens be the source of truth for meaning, and component tokens be the place where customization lives without polluting the layers below.

Naming conventions that survive

Use camelCase or kebab-case consistently. Pick one. Document the choice. The bikeshedding is not worth the time.

Use a category prefix. color, space, font, radius, shadow, motion. The category should be the first segment of the name. This makes tokens easy to filter in autocomplete and easy to grep across the codebase.

Avoid abbreviations. text.subtle reads better than txt.sub. Spell tokens out. The autocomplete handles the typing for you.

Use scale suffixes consistently. font.size.xs, sm, md, lg, xl, 2xl, 3xl. The same scale convention applies to space, radius, and shadow. If the design needs odd one-off values, that is a sign your scale is wrong, not a sign you need to add irregular tokens.

Theming without runtime cost

Define semantic tokens as CSS custom properties on a root selector. Define theme overrides on data-theme or [data-mode] selectors. The browser's CSS engine handles theme switching natively, with no JavaScript runtime cost and no flash on theme change if you set the initial theme before paint.

For dark mode, this looks like: :root { --surface-canvas: #ffffff; } [data-theme='dark'] { --surface-canvas: #0a0a0b; }. Components use var(--surface-canvas) and never know about the theme. The theme attribute is set on the html element by a tiny inline script in the head, before paint.

For brand themes, the same pattern. [data-brand='client-a'] overrides whichever semantic tokens differ from the default. Components stay theme-agnostic. The CSS is doing the work the JavaScript would have done in a previous era.

Style Dictionary vs custom pipelines

Style Dictionary is the default I reach for. It transforms a JSON or YAML token file into CSS, JavaScript, iOS, Android, and any other format you need. The transform pipeline is configurable and the community has shipped most of the transforms you actually need.

Custom pipelines win when you have a bespoke source of truth (a Figma plugin output, a database, a content management system) or when the build is small enough that maintaining Style Dictionary's plugin surface is more work than writing a 100-line script. I have shipped both. The tradeoff is maintenance: Style Dictionary handles a lot of edge cases for free, and a custom script will eventually need to handle them for itself.

Figma sync, honestly

Figma Variables released in 2023 finally made design tokens first-class in Figma. The hard problem is keeping Figma and code in sync. Two-way sync is harder than vendors claim — Figma's plugin API is restrictive, and round-tripping changes through a CI pipeline introduces lag and conflicts.

The pragmatic answer is one-way sync from code to Figma. Code is the source of truth. A Figma plugin reads the token file from a Git repository and applies the variables. Designers work with the synced variables. When designers want to change a token, they file a PR (or send a Slack message that becomes a PR), and engineering merges it. The flow is slower than designers want and more reliable than every two-way sync I have shipped.

Tokens for motion and easing

Motion tokens get neglected. Most teams have color and space tokens but every animation in their codebase uses different durations and different easing functions. The result is an interface that feels uncoordinated even when individual components look polished.

Define duration tokens: motion.duration.fast (120ms), default (180ms), slow (240ms), slower (360ms). Define easing tokens: motion.ease.out (cubic-bezier(0.16, 1, 0.3, 1)), in-out, spring. Components use the tokens. The design system enforces a consistent motion grammar without requiring every developer to remember which curve goes where.

Migration when you adopt this late

Most teams adopt design tokens after the codebase has shipped without them. The migration is mechanical: codemod or grep-and-replace raw values with token references, ship the change in chunks, watch for visual regressions in screenshot tests.

The political part is harder. Getting a team to commit to using tokens requires convincing them that the tokens save time. The win is real but it is on the order of weeks, not seconds. Set up the lint rules that flag raw values, write the codemods that migrate the existing code, and pair with the team on the first few PRs that adopt the system. After that, it carries itself.

Closing

Token systems work when they are boring. The interesting decisions happen at the semantic layer — what surfaces does your interface have, what hierarchy do your text styles express, what motion grammar does your product use. The primitives are scaffolding. Build them well, name them carefully, and stop iterating on the token file once the system is shipping. Iterate on the components instead. That is where the user actually meets your design system.