"Design tokens and CSS variables do the same thing, right?" That question comes up often, and the confusion is understandable—both store reusable design values. But they operate at different levels of abstraction, and conflating them leads to design systems that are hard to extend and impossible to share across platforms.
The short version: design tokens are the source data. CSS variables are one possible output format.
CSS Variables: A Browser Feature
CSS custom properties—commonly called CSS variables—landed in browsers in 2016. They let you define a value once and reference it everywhere in your stylesheets:
:root {
--color-primary: #3b82f6;
--spacing-base: 8px;
}
.button {
background: var(--color-primary);
padding: calc(var(--spacing-base) * 2);
}
The browser calculates these values at render time. You can override them with JavaScript, scope them to a specific selector, and change the entire UI by updating a handful of :root declarations. No build step required.
That simplicity is their strength and their limit. CSS variables are browser-native strings. They have no types, no descriptions, and no structure beyond what you create manually. A variable named --blue-500 is just a string with a color-ish name. Nothing prevents you from using it for a border, a shadow, or a hover state—even if it was intended as a primary button background. Nothing stops a developer from introducing --blue-500-new next to it.
They also can't leave the browser. You can't generate iOS Swift constants or Android color resources from a CSS file.
Design Tokens: Structured Source Data
Design tokens are named design decisions stored in a platform-agnostic format—typically JSON. They represent the canonical source of truth for your design system and carry enough structure to be transformed into any platform format:
{
"color": {
"primary": {
"value": "#3b82f6",
"type": "color",
"description": "Primary brand color. Use for CTAs, links, and focus states."
}
},
"spacing": {
"base": { "value": "8px", "type": "dimension" },
"md": { "value": "16px", "type": "dimension" }
}
}
The type field tells build tools how to transform the token. A color token becomes a hex value in CSS, a UIColor in Swift, a <color> resource in Android XML. The description field tells humans and AI tools what the token is for, not just what it is.
This structure is what makes design tokens valuable beyond simple reuse. They're queryable, validatable, and transformable in ways a raw CSS file isn't.
The Relationship: Source and Output
The mental model to hold is this:
Design Tokens (JSON)
│
├──► CSS Variables → Web
├──► Swift constants → iOS
└──► XML resources → Android
Design tokens are the single source of truth. CSS variables are one derivative output. When you change a token value in JSON and run a build, every platform format updates in sync.
This matters practically. If your design system only exists as CSS variables, you can't share it with a mobile team. You can't give AI tools structured, typed, described context about your design decisions. You can't validate that a token is a valid color value before it ships.
When Each One Fits
CSS variables excel at runtime concerns. User toggles dark mode? Update document.documentElement.setAttribute('data-theme', 'dark') and the CSS variables under that selector take over—no rebuild, no delay. Dynamic spacing based on viewport size? CSS calc() and custom properties handle that naturally. Quick prototype with no build tooling? CSS variables work directly in a browser.
Design tokens excel at authoring and distribution concerns. Building a multi-platform product that needs to share design decisions between web and mobile? Tokens are the only path that doesn't require duplicating work. Want AI code generation to produce components using your actual color palette instead of arbitrary Tailwind defaults? Tokens give AI the structured, semantic context it needs. Need to enforce that --color-primary is always a valid hex or OKLCH value? Token schemas validate at build time.
The practical answer for most product teams is both: tokens as source, CSS variables as the web output.
How FramingUI Generates Both
FramingUI takes design tokens as input and generates everything your stack needs from a single source:
{
"color": {
"primary": {
"500": { "value": "#3b82f6", "type": "color" }
}
}
}
From this, the build step produces CSS variables with both light and dark mode variants, a Tailwind config that references those variables, TypeScript type definitions for token names, and the metadata the MCP server uses to answer AI queries.
The AI integration specifically depends on the token structure, not the CSS output. When Claude Code asks the MCP server "what colors are available?", it gets back the JSON token data—names, values, types, and descriptions. That's what lets it choose bg-primary-500 instead of bg-blue-500, and know that primary-500 is appropriate for a call-to-action rather than a background.
Migrating from CSS Variables to Tokens
If you already have a CSS variable system and want to move to a token-based workflow, the migration is mechanical.
Take your existing variables and extract them to JSON:
/* Before */
:root {
--color-blue-500: #3b82f6;
--spacing-md: 16px;
}
{
"color": { "blue": { "500": { "value": "#3b82f6", "type": "color" } } },
"spacing": { "md": { "value": "16px", "type": "dimension" } }
}
Then add semantic aliases that reference the primitive values:
{
"color": {
"primary": {
"value": "{color.blue.500}",
"type": "color",
"description": "Primary brand color"
}
}
}
Run a build step to generate CSS variables from the tokens. The output looks identical to what you had before—but now it's generated from a structured source that you can transform, validate, share, and query.