Token files drive everything — the CSS, the Figma variables, two independent brands. Three weeks lost to a naming mistake. One pipeline that made it worth it.
Approach
The JSON file is the source of truth — not Figma. Style Dictionary compiles it to CSS; the same names flow into Figma variables. A token name in two places with different spellings is a hidden handoff step in disguise. Building the visual layer from scratch meant no inherited conventions — and no inherited mistakes. The cost came later: three weeks spent not on components, but on names.
Primitive values
tokens/global.json
color.neutral-900#1a1a1bspace.524pxfont-weight.bold700Component decisions
tokens/brands/portfolio/tokens.json
color-text-primary{color.neutral-900}color-accent-default{color.teal-500}space-component-gap{space.5}Brand overrides
tokens/brands/[brand]/tokens.json
color-accent-default{brand-a.indigo-400}font-family-heading'DM Serif Display'color-surface-primary{brand-a.background-0}Primitive values never appear in component CSS — only semantic tokens do.
Design decisions
Most token-first systems start in Figma: set the variable, export it, hope the handoff holds. I inverted that. The JSON file is authoritative. Style Dictionary compiles it to CSS. A separate step pushes the same names into Figma. Figma shows you whether the pipeline worked. If Figma and CSS disagree, CSS is right.
Technical decisions
BEM with CSS custom properties: class names describe structure, tokens carry all visual values. Swap the brand class and every component re-resolves through a different token set — no logic, no JavaScript, no build change. Radix handles the one category I chose not to build: interactive components that need accessible behaviour by specification. Everything visible is mine.
Next.js App Router
Framework — pages, routing, server and client components
BEM + CSS Custom Properties
Component structure and visual values — driven entirely by tokens
Style Dictionary
Compiles token JSON to CSS — single build output: styles/brands/portfolio.css
React + Radix UI
Stateless presentational components — Radix only for accessible primitives
Storybook + Chromatic
Component review across both brand contexts — visual regression per commit
One stack. One source of truth. One codebase.
Where design met code
The Figma variable panel holds the same names as the Style Dictionary source. Not because they were synced — because they were compiled from the same file. The pipeline never interprets. It copies.
The compiled CSS custom property carries the same name. Three sources, one namespace. When all three agree — as the block below shows — there is nothing left to translate. That is what the absence of a handoff step looks like.
button-primary-background = #15616Dbutton-primary-background: #15616D--button-primary-background: #15616DFigma
Component designed with variants, tokens, and correct naming convention
Console MCP
Exposes Figma structure and token data directly to Claude Code — no screenshots
Claude Code
Reads CLAUDE.md and token files, generates BEM component and Storybook story
Storybook
Review props, variants, and states across both brand contexts via brand switcher
Chromatic
Visual regression sign-off per commit — snapshots compared per brand automatically
GitHub
Commit reviewed component — production-ready, token-compliant, system-aligned
Design to code. No manual translation.
Outcome
271 semantic tokens across three tiers — global primitives, semantic decisions, brand overrides. No hardcoded values anywhere in component code. Every visual property resolves through the pipeline.
Two independent brand themes share one component codebase. Switching brands is a single CSS class swap — no component logic, no JavaScript, no duplicated components. Add a third brand: write a token file.
Figma variables map 1:1 to Style Dictionary token names. The pipeline compiles — it doesn't interpret, negotiate, or drift. When Figma and CSS agree, that agreement is structural, not maintained.
Where this could go
This is the version I'd design next.
Reflection
I added tokens as I needed them. That worked for the first few weeks. Then the system had to support three tiers simultaneously — global primitives, semantic decisions, brand overrides — and the names I'd used for the first tier made no sense for the second. color-gray-100 makes an appearance claim. neutral-100 describes a position. Nothing visual changed. The language did, completely. In a token-first system, naming is architecture — get it wrong and you don't fix the name, you refactor the system.
An agent generating a new component needs to know which token to reach for. One enforcing brand consistency needs to verify the right names are in use. One proposing a palette change needs to understand which tier it's operating in. All of that requires stable, unambiguous language. The naming work wasn't just architecture. It was the condition for safe AI participation.
Before agents can coordinate a design system, the design system has to know what its own words mean.
Most portfolios build something, then document the system behind it. This one is the system. Every visual decision runs through the same token pipeline — the dark and light mode toggle in the navigation is that same system running live, documented step by step in the DS Playbook. It also turned out to be the prerequisite for the next problem: a language stable enough for agents to act on safely.
Open the inspector. It's all tokens.
The foundations, running
Explore the full design system →Typography
Color
Spacing