Design Tokens vs CSS Variables: What’s the Difference?
Two files are open side by side. tokens.json says
"brand-600": { "$value": "#2563eb", "$type": "color" }; variables.css
says --color-brand-600: #2563eb;. Same value, nearly the same name — and
the natural question is which one you are supposed to be writing.
The answer: they are not competitors, they are source and output. Design tokens are the source — design decisions stored in a tool-neutral file. CSS custom properties are one export — the same decisions rendered for browsers. A useful analogy comes from compilers, and it is an analogy: the tokens file is source code, the stylesheet is one build target, and Figma Variables or native resources are sibling targets generated from the same source. The distinction sounds academic and is not; it decides what can consume your decisions, what tooling can check, and who reviews what. The source side is covered across our design tokens guide — this article is about the boundary itself.
What can tokens reach that CSS variables cannot?
Non-browsers. A custom property is a CSS feature: it reaches everything a
stylesheet reaches and nothing else. The decisions in a product reach
further — the same blue and the same spacing need to hold in the Figma
file, the iOS and Android apps, the email templates, the slide deck the
sales team exports. If variables.css is the source of truth, every one
of those consumers is fed by hand-transcription, and hand-made copies
drift.
A tokens file feeds them all as generated views: custom properties for the web, Figma Variables for design, platform resources for native. The day a second consumer appears is the day the difference stops being theoretical — with one consumer the two approaches are nearly indistinguishable; with two, only one of them has a source.
What does a tokens file know that a stylesheet doesn’t?
Meaning. To CSS, --brand-600 is an untyped value that gets substituted
where it is used; nothing in the language knows it is a color, and a typo
inside it fails silently at use time. A tokens file knows more: $type
says the value is a color, $description says what it is for, and a
reference records that accent is defined as {color.brand.600} rather
than coincidentally equal to it.
That knowledge is what tooling runs on. A build can validate the file — reject a malformed color or a reference to a token that doesn’t exist — before anything ships. Documentation can be generated instead of written. Transforms can be applied mechanically: px to rem, hex to OKLCH, one source rendered per target’s conventions. None of this is available to a bare stylesheet, because the stylesheet stores answers without their questions.
How does a token become a CSS variable?
Mechanically. Three tokens as DTCG source:
{
"color": {
"brand": { "600": { "$value": "#2563eb", "$type": "color" } }
},
"spacing": {
"4": { "$value": "23px", "$type": "dimension" }
},
"semantic": {
"accent": { "$value": "{color.brand.600}", "$type": "color" }
}
}
And the :root block generated from them:
:root {
--color-brand-600: #2563eb; /* oklch(0.546 0.215 262.9) */
--spacing-4: 23px;
--color-accent: var(--color-brand-600);
}
Names become property names, values carry over, and — the detail worth
noticing — the reference survives as var(): the export preserves the
structure of the decision, not just its current answer. Components then
consume the output normally:
.card {
padding: var(--spacing-4);
background: var(--color-accent);
}
See source and exports side by side in Scale Composer — the DTCG file on one side and the same names rendered as CSS custom properties, a Tailwind v4 theme and Figma Variables on the other; edit the source and all three outputs move together.

Do tokens and variables live different lifecycles?
Yes, and the difference shows up in code review. Tokens are
design decisions: they
get proposed, reviewed, versioned and diffed — a pull
request that changes accent from one primitive to another is a design
change wearing a reviewable diff, and both designers and developers have
standing to comment. The generated variables.css is implementation
detail: a build artifact that is regenerated, not edited. Hand-editing a
generated stylesheet has the same lifespan as hand-editing any compiled
output — until the next build.
The practical rule that falls out: review the source, generate the target, and let no value enter the stylesheet that didn’t come through the source.
When are CSS variables alone honestly enough?
When there is one consumer and there may only ever be one. A
single-platform web product with no Figma handoff, maintained by the
people who wrote it, loses little by declaring its custom properties
directly — the file is simpler, the toolchain shorter, and a disciplined
:root block with role-named properties captures much of the naming
benefit without any token infrastructure. That is a legitimate choice,
not a lesser one.
The token layer earns its cost when consumers multiply: the first Figma library that should match the code, the first native surface, the first second theme. Adopting the machinery before then is paying for reach you don’t yet use — reasonable as preparation, but not obligatory.
What do CSS variables add at runtime?
One thing tokens alone cannot: live re-resolution in the browser. Custom properties resolve through the cascade — the mechanics are documented in MDN’s guide to CSS custom properties — so re-declaring them under a scope switches every consumer at once, without recompiling anything:
[data-theme="dark"] {
--color-accent: oklch(0.70 0.14 262.9); /* re-derived dark value */
}
Every var(--color-accent) on the page re-resolves the moment the
attribute lands. This is a property of the output format, not of the
source — a reason the CSS export is the right target even when tokens are
the source, and the mechanism theme systems are built on. What values the
dark theme should hold is
its own subject; here it is enough
that the switch costs one declaration block.
Generate your variables from a source
The boundary is clearest when you cross it deliberately. Export CSS custom properties from a token source — change one decision on the source side (a seed, a scale step), re-export, and read the diff on the CSS side: the variables update, the reference structure holds, and nothing needed editing by hand.