Product Design

Design tokens vs CSS variables vs Tailwind: what each one solves

Design tokens, CSS variables, and Tailwind get conflated in 2026 stack debates. They sit at three different layers and the choice is rarely either or.

April 22, 20268 min read
Design tokens vs CSS variables vs Tailwind: what each one solves

A design token is a platform-agnostic record of a design decision (a color, a spacing step, a radius) stored in a structured format that can compile to CSS, Swift, Kotlin, XML, or a Tailwind theme. CSS variables are the browser-native primitive that holds those values at runtime. Tailwind is a utility framework that, since version 4, generates classes from CSS variables you declare in a theme block. The three sit at three different layers of the same stack and the comparison only makes sense once you stop treating them as alternatives.

This article is for product teams about to pick a styling architecture for a new SaaS or considering a migration. We map the three concepts to the layer they actually occupy, list where each one is the right tool, name the failure modes that come from confusing them, and end on the choice we make at Studio and why.

TL;DR for the impatient

Pick design tokens (in the W3C DTCG format) when more than one platform has to consume the same design decisions, when the design team works in Figma with Tokens Studio, or when you want a single source of truth that survives a framework change. Use CSS variables as the runtime implementation of those tokens for the web. Reach for Tailwind when the team values speed and a shared utility vocabulary on a small to mid surface, and accept that v4 made it a thin layer over your CSS variables rather than a parallel system. Skip Tailwind when the project carries a mature design system, runs across many consumer apps, or measures every kilobyte of CSS shipped.

The three layers, plainly

Design tokens are a format, not a feature

The W3C Design Tokens Community Group (DTCG) published the first stable version of the Design Tokens Format Module on October 28, 2025. The format is JSON, properties are prefixed with $, and files use the .tokens or .tokens.json extension with media type application/design-tokens+json. A token file is a vendor-neutral document that any compatible tool (Figma, Penpot, Sketch, Framer, Knapsack, Supernova, zeroheight, Tokens Studio, Style Dictionary, Terrazzo) can read or write.

{
  "color": {
    "brand": {
      "primary": { "$type": "color", "$value": "#3D5AFE" }
    }
  },
  "radius": {
    "md": { "$type": "dimension", "$value": "8px" }
  }
}

That file does nothing on its own. A build tool (Style Dictionary is the reference implementation, Terrazzo is the modern challenger) compiles it to whatever the target needs: CSS custom properties for the web, a Swift extension for iOS, a Kotlin object for Android, a JS module for React Native, a Tailwind theme. The token is upstream of every implementation.

CSS variables are the web runtime

CSS custom properties (the spec name; everyone says CSS variables) are scoped to the cascade, can be overridden at any selector, and read by any property that accepts the value type. They are the only mechanism in the browser that lets a single declaration be themed, swapped at runtime, or read from JavaScript without recompiling.

:root {
  --color-brand-primary: #3D5AFE;
  --radius-md: 8px;
}

[data-theme="dark"] {
  --color-brand-primary: #8C9EFF;
}

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

If you have ever shipped a working dark mode without a build step, you used CSS variables. They are not a design system, they are the wire between your tokens and the pixels.

Tailwind is a framework that consumes CSS variables

Tailwind CSS v4, released in early 2025, rewrote the engine in Rust (Oxide), removed the JavaScript config file, and moved the design system into a CSS-native @theme block. The values you declare in @theme become both CSS variables and the source for generated utility classes.

@import "tailwindcss";

@theme {
  --color-brand-primary: #3D5AFE;
  --radius-md: 8px;
}

/* generates: bg-brand-primary, text-brand-primary, rounded-md, etc. */

The change matters. In v3, design tokens lived in tailwind.config.js and were trapped in the build step. In v4, they live as real CSS variables that DevTools can read and other CSS can reference. Tailwind became a utility generator on top of variables you control, instead of a parallel ecosystem.

Comparison table

DimensionDesign tokens (DTCG)CSS variablesTailwind v4LayerSource of truthBrowser runtimeUtility generatorWhere they liveJSON files (.tokens.json)Inside CSS, scoped to selectors@theme block in CSSCross-platformYes (web, iOS, Android, RN)Web onlyWeb onlyRuntime themingThrough compiled CSS variablesNative, no rebuildNative via underlying variablesDesigner-friendlyYes (Figma plugins read DTCG)No, developer territoryPartial, requires utility literacyBuild stepRequired (Style Dictionary, Terrazzo)NoneRequired (Tailwind compiler)Lock-in riskLow, vendor-neutral specNone, web standardMedium, framework-specific syntaxOutput sizeWhatever you compile toBytes you writeOnly utilities you actually use

Where design tokens win

Tokens earn their place the moment a second platform enters the picture. A native iOS app that has to share brand colors with the web cannot consume CSS, but it can consume a Style Dictionary build that emits a Swift file from the same JSON the web consumes. A design team working in Figma with Tokens Studio writes DTCG natively; without that layer, every brand update becomes a manual port. Multi-brand or multi-tenant SaaS benefits the same way: each tenant ships a token set, Style Dictionary compiles per-tenant CSS variable bundles, the runtime stays identical.

Tokens also act as a contract between design and engineering that does not depend on tooling fashion. The DTCG format reaching stable status in late 2025 means a token file written today should still parse in five years, regardless of which framework owns the frontend.

Where CSS variables win

CSS variables win the runtime. They are the only sane way to ship dark mode, density modes, brand themes, or per-tenant skins without a rebuild. They are zero-dependency, zero-build, and supported in every browser that matters. Reading a variable from JavaScript with getComputedStyle works without a config file. Overriding a variable in a media query swaps tokens without touching markup.

For a small marketing site or a single-brand product with no design tool integration, CSS variables alone are often enough: write them in a :root block, document them in a README, ship. The DTCG format becomes overhead if no second consumer exists.

Where Tailwind wins

Tailwind earns its place when team velocity and shared vocabulary matter more than CSS architecture. A small team prototyping a new product can move faster with utility classes than with a custom design system, because the cost of naming components is zero. Tailwind v4 added the @theme bridge, so the tokens you declare are accessible to non-Tailwind CSS too, which removes the worst of the v3 lock-in.

Tailwind also ships a respectable default token set (spacing scale, color ramp in OKLCH, type scale) that a junior team can lean on while learning. For brochure sites, internal tools, and products with no formal design system, Tailwind is a defensible default.

The three failure modes from confusing them

Treating Tailwind as a design system

Tailwind generates utilities. It does not enforce semantic naming, document component contracts, or audit token misuse across consumer projects. A team that calls Tailwind their design system tends to ship hardcoded values inside utility classes (bg-[#3D5AFE], p-[17px]) within months. The fix is to forbid arbitrary values in lint, declare every value in @theme, and treat the theme block as the token registry.

Skipping the token layer because CSS variables exist

A team that goes straight from Figma to CSS variables loses the only artifact that survives a framework migration. The CSS file is the implementation, not the source. When the second platform shows up (mobile app, design tool sync, partner brand), the team is back to manual porting. The fix is to write the token JSON first even if the only consumer today is the web; Style Dictionary takes a few hours to set up.

Treating tokens as a JSON copy of the CSS

Tokens that mirror the CSS one-to-one (color-blue-500 in JSON, --color-blue-500 in CSS) miss the point. Tokens carry semantic meaning (color.action.primary, color.surface.elevated) that maps to many concrete values across themes and platforms. A flat color palette in JSON is a color palette, not a token system.

What we use at Studio and why

Studio runs an internal design system that ships as an NPM package and powers more than a dozen consumer projects. We define tokens in JSON, compile them to CSS variables (the --ds-* namespace) with a Style Dictionary build, and consume them in CSS Modules and component CSS. We removed Tailwind from production three years ago after it crossed 50% of bundle weight on a project that needed sub-1s LCP everywhere.

The reasoning was not aesthetic. It was that the design system already enforced naming, documented every component contract, and could lint for token misuse across all consumer projects from a central manifest. Tailwind became a parallel naming system that diluted the contract. For new client work where there is no internal design system to lean on and the project ships in weeks, we still recommend Tailwind v4 with a strict @theme block and arbitrary-value lint disabled.

The decision tree we apply: more than one platform, or design tools that speak DTCG natively, or runtime theming across tenants, then the JSON token layer is non-negotiable. Web only, single brand, design system maturity above three years, then CSS variables compiled from tokens are enough. Web only, no design system yet, team under five people shipping in weeks, then Tailwind v4 with token discipline is the fastest defensible path.

Sources

Photo by Noemí Jiménez on Unsplash

Frequently asked questions

Do I still need design tokens if I use Tailwind v4?
Only if a second consumer exists. Tailwind v4 already turns your @theme block into CSS variables, which is enough for a single-platform product. The moment a native mobile app, a partner brand, or a Figma sync needs to read the same values, you need the JSON token layer above Tailwind. The JSON file becomes the source; Tailwind becomes one compiled output.
Can I migrate from Tailwind v3 to design tokens without rewriting components?
Mostly yes. Move the values from tailwind.config.js into a DTCG JSON file, run Style Dictionary to emit CSS variables and a Tailwind v4 @theme block, and ship both. Components that reference Tailwind utilities keep working; components that reference CSS variables work too. The migration cost is in the build setup, not the components.
Are CSS variables slow at scale?
No. CSS custom properties are evaluated by the browser engine in C++, not in user JavaScript, and are heavily optimized. The performance concerns from 2017 era benchmarks were resolved by every major engine. The real cost in modern stacks is bundle size of the CSS file, not runtime variable resolution.

Studio

Start a project.

One partner for companies, public sector, startups and SaaS. Faster delivery, modern tech, lower costs. One team, one invoice.