Skip to content

Guide · glossary

design tokens vs CSS variables vs theme: what's the difference?

July 4, 20267 min read

Three words get used as if they were the same thing: token, variable, theme. They are not. A design token is a decision. A CSS variable is one way to deliver that decision to a browser. A theme is a coordinated set of decisions you swap all at once. Most explainers stop at "tokens are not CSS variables" and leave theme dangling, which is exactly where the confusion sits. This post pins all three down with code.

The quick version: the token is the source of truth, the CSS variable is one output of it, and the theme is what you get when a whole set of tokens changes value together. You can have tokens with no CSS variables, and CSS variables that are not tokens. A theme is neither; it is a mode you flip.

a design token is the decision

A design token is a named value that records one design decision, stored in a format that does not care what platform consumes it. The Design Tokens Community Group, the group behind the emerging cross-tool standard, defines a token at minimum as a name paired with a value. What matters is where it lives: not in your stylesheet, but in a neutral file, usually JSON, that compiles out to CSS, Swift, a Tailwind config, or a Figma library.

Under the Design Tokens Format Module, which reached its first stable version (2025.10) in October 2025, a token is a JSON object with a $value and a $type:

{
  "color": {
    "action": { "$value": "#0066ff", "$type": "color" }
  },
  "radius": {
    "md": { "$value": "8px", "$type": "dimension" }
  }
}

That file is the decision. It knows nothing about the browser. A build step (Style Dictionary, Terrazzo, Tokens Studio) reads it and emits whatever each platform needs. On web, one thing it commonly emits is a CSS variable, which is the next box.

a CSS variable is one delivery mechanism

A CSS custom property, informally a CSS variable, is a browser-native primitive that holds a value at runtime.

:root {
  --color-action: #0066ff;
  --radius-md: 8px;
}

.button {
  background: var(--color-action);
  border-radius: var(--radius-md);
}

This lives inside CSS. It is scoped to the DOM, it cascades, and the browser recalculates styles the instant it changes. That last property is why CSS variables matter for theming: they are live. Change the value and every rule reading it updates with no rebuild.

Here is the distinction most articles get right and then blur: the token is the source of truth, the CSS variable is one output of it. As the DEV community piece "Design Tokens Are Not CSS Variables" puts it, tokens live in a format that can compile into CSS, Swift, or Kotlin, while the CSS variable is the web-specific artifact. Penpot's developer guide frames it the same way.

So the two are not competitors. In a web-only project with no design tool and no native app, you can skip the JSON layer and write CSS variables by hand. They still act like tokens in spirit (named, reused, changed in one place); they are simply not portable, because they only exist inside CSS. The moment the same decision must reach iOS, Android, and Figma, you promote it to a real token file and let CSS variables become an output.

design tokenCSS variable
lives inplatform-neutral file (JSON)a CSS stylesheet
known bydesign tools, build pipeline, every platformthe browser only
rolesource of truth for a decisionone runtime delivery of that decision
portable to iOS / Androidyes, via a build stepno, CSS only
live at runtimeno, it is datayes, browser recalculates on change

a theme is a coordinated set you swap

A theme is not a third kind of variable. It is what happens when you redefine a whole group of tokens together so the interface shifts as a unit: light to dark, brand A to brand B, one tenant's skin to another's.

The trick that makes theming clean is a two-tier token structure. You split tokens into primitives and semantics. Primitive (also called global) tokens hold raw values with no opinion about use, like blue.500 = #0066ff or gray.900 = #111111. Semantic (also called alias) tokens point at a primitive and add intent, like color.text.primary or color.action. Components only ever read the semantic layer, never a raw hex.

Now a theme is just a redefinition of the semantic layer. As the "Semantic vs Primitive Tokens" writeup explains, a dark theme redefines color.text.primary to reference gray.100 instead of gray.900 while the primitive palette stays untouched, and every component consuming that token picks up the right value. In CSS variables, that looks like this:

:root {
  --gray-900: #111111;
  --gray-100: #f5f5f5;
  --color-text-primary: var(--gray-900);
}

[data-theme="dark"] {
  --color-text-primary: var(--gray-100);
}

Because CSS variables are live, flipping data-theme on the root re-points --color-text-primary and the browser repaints everything downstream instantly. No rebuild, no second stylesheet, no JavaScript touching individual elements. That is why CSS variables are, practically, the only sane way to ship dark mode, density modes, or per-tenant skins, and why the token structure underneath them makes a theme swap a one-line change instead of a find-and-replace.

So the three sit in a stack: the token is the decision, the CSS variable is how it reaches the browser, and the theme is a named set of decisions that changes together. Get the layering right and dark mode is trivial. Get it wrong (raw hexes in components, no semantic layer) and every theme becomes a manual sweep.

when to reach for each

  • Web-only, no design tool, no native app: write CSS variables directly. You do not need a JSON pipeline to name a color once and reuse it. Keep a semantic layer anyway so theming stays easy.
  • Same decisions must reach Figma, iOS, Android, or multiple codebases: author real design tokens in a neutral file and compile CSS variables out as one target. This is where portability pays for the extra build step.
  • You need dark mode, brand skins, or density modes: structure tokens as primitive plus semantic, deliver them as CSS variables, and let a theme redefine the semantic layer on a root attribute.

For a deeper walk through what a token is on its own, what is a design token covers the six token families and the primitive-semantic-component tiers.

the harder question: what are the real tokens on a site you admire?

All of the above assumes you already know your values. The more common situation is staring at a site you want to learn from and having no idea what its actual tokens are. The blue you see might be one primitive reused fifteen times, or three near-identical blues that drifted. The radius might be a single radius.md or a mess of one-offs. You cannot theme, or rebuild, what you have not named.

This is where reading tokens back off a live page helps. uiscanner takes any public URL and returns the tokens (colors, fonts, type scale, radii, spacing, shadows, motion), a section-by-section structure, and a Claude-tailored build prompt to recreate the look. It reads the rendered page with vision plus the DOM, so it catches what a code-only scraper misses: the real primary button versus a one-off link, a centered versus split hero, decorative imagery it describes as prompts to recreate as originals. It describes and tokenizes; it never copies or re-hosts the original site's asset bytes, so you rebuild in your own stack from the values.

Every scan returns an id and a shareable uiscanner.com/t/<id> link. To see the output shape, the Stripe teardown shows a real token set pulled off one of the most-studied sites on the web. To pull tokens yourself:

npx -y uiscanner-mcp@latest scan stripe.com --target landing

Or wire it into Claude Code, Cursor, or Codex as an MCP server so an agent can request a teardown mid-build:

claude mcp add uiscanner -- npx -y uiscanner-mcp@latest

That runs locally over stdio. Quota is unified across the web app, MCP, and CLI on a rolling 30-day window: Free is 5 scans, Indie ($12/mo) is 100, Studio ($39/mo) is 400.

the short version

A design token is a decision, stored where every platform can read it. A CSS variable is one way to deliver that decision to a browser, live and cascading. A theme is a coordinated set of decisions you swap together, which works cleanly only when your tokens split into primitives and semantics and your components read the semantic layer. Three different things doing three different jobs, and knowing which one you need is most of the battle. The other half is knowing what your values really are, which is the one part you can read straight off a page instead of guessing at.

Sources

uiscanneruiscanner

Paste any public URL and keep what makes it look good. You get the tokens, the structure, and a prompt you can build from.

Scan another site

Paste any public URL and get its tokens, structure, and a build-ready prompt.

© 2026 uiscanner. All rights reserved.