Technology·14 min read

Design Tokens vs CSS Variables vs Tailwind Config: System Guide

Your design system needs a single source of truth for colors, spacing, and typography. The real question is whether that source lives in JSON tokens, CSS custom properties, or your Tailwind config. Here is how to decide.

Nate Laquis

Nate Laquis

Founder & CEO

Why Your Design System Needs a Token Strategy

Every production design system eventually hits the same wall. Colors are defined in Figma, redefined in a Tailwind config, redefined again in a mobile stylesheet, and overridden inline by three developers who each assumed their file was the source of truth. The result: your brand blue is four slightly different shades across platforms, dark mode is a mess, and white-labeling a customer theme takes weeks instead of hours.

Design tokens, CSS custom properties, and Tailwind config values all solve variations of this problem. But they solve it at different layers of the stack, with different trade-offs, and they are not mutually exclusive. In fact, the strongest design systems in 2026 combine all three.

This guide breaks down each approach on its own merits. Then we will look at how they compose together, where each one shines, and the specific combinations I recommend based on whether you are building a single web app, a multi-platform product, or a white-label SaaS.

Design system code architecture with color tokens and variables on screen

If you have already chosen a component library and want to layer tokens on top, our ShadCN vs Mantine vs Chakra comparison pairs well with this article. The token strategy you pick here will influence how you theme those components.

Design Tokens: The Platform-Agnostic Source of Truth

Design tokens are the most abstract layer. They store your design decisions (colors, spacing, typography, border radii, shadows, motion curves) in a platform-agnostic format, typically JSON or YAML. A build tool then transforms those tokens into whatever output your platforms need: CSS custom properties for the web, XML resource files for Android, Swift asset catalogs for iOS, or even Compose theme objects for Kotlin Multiplatform.

Style Dictionary from Amazon remains the most mature token pipeline in 2026. You define tokens in JSON with a nested hierarchy. A color token might look like this: a "color" group containing a "brand" group containing a "primary" key with a value of "#2563EB" and optional metadata for description, type, and Figma reference. Style Dictionary reads those JSON files and runs them through transforms and formatters that output platform-specific code. One input, many outputs.

Tokens Studio (formerly Figma Tokens) is the bridge between designers and this pipeline. It lets designers define and edit tokens directly inside Figma, then syncs them to a Git repository as JSON files. Those files feed into Style Dictionary or any custom build. The workflow becomes: designer changes a token in Figma, pushes to a branch, CI runs the token pipeline, and platform-specific outputs update automatically. This is genuinely powerful for teams where design and engineering need to stay in sync without manual handoffs.

When tokens are the right choice

  • Multi-platform products. If you ship web, iOS, and Android from the same brand, tokens are non-negotiable. CSS variables only help on the web. Tokens help everywhere.
  • White-label or multi-tenant theming. When each customer gets their own brand colors and typography, tokens let you swap an entire theme by swapping a JSON file. The build pipeline handles the rest.
  • Large organizations with multiple teams. Tokens create a contract between design and engineering. The token names become the shared vocabulary. Nobody argues about hex codes in pull requests.
  • Design system libraries published as packages. If your design system ships as an npm package consumed by multiple apps, tokens give you a clean API for theming that does not depend on CSS specificity or build tool configuration.

Where tokens fall short

Tokens add build complexity. You need a pipeline (Style Dictionary config, CI step, versioning strategy). For a single web app with one brand, that overhead may not pay for itself. Tokens are also static at build time by default. They do not give you runtime theming unless you transform them into CSS custom properties or JavaScript variables that can be swapped on the fly. The abstraction is valuable, but it is an abstraction, and abstractions have costs.

CSS Custom Properties: Native, Cascading, and Runtime-Ready

CSS custom properties (the technical name for CSS variables) are the browser-native solution. You declare them with the double-hyphen syntax on a selector, typically :root for global values. They cascade through the DOM just like any other CSS property. They can be overridden at any level of the tree. And they update at runtime without a rebuild.

That last point is what separates CSS custom properties from the other two approaches. Tokens are build-time. Tailwind config values are build-time (or compile-time in v4). CSS custom properties are live. You can change them with JavaScript, toggle them with a class swap, or scope them to a specific component subtree. This makes them the natural tool for runtime theming: dark mode toggles, user-selected accent colors, or per-section color schemes on a marketing site.

The cascading advantage

The cascade is the feature that developers either love or misunderstand. When you set a custom property on a parent element, every child inherits it unless overridden. This means you can scope themes to regions of your page. A dashboard sidebar can use one color scheme while the main content area uses another, just by setting different custom property values on their container elements. No build step, no class duplication, no JavaScript framework required.

For dark mode specifically, CSS custom properties are the cleanest implementation. You define your semantic color tokens as custom properties on :root, then override them inside a .dark class or a prefers-color-scheme media query. Every component that references those properties updates automatically. No conditional class logic in your components. No theme provider wrappers. Just CSS doing what CSS was designed to do.

Developer laptop showing CSS custom properties and theme variables in code editor

Practical patterns

The pattern I recommend for most web apps: define your semantic tokens as custom properties on :root. Use names that describe purpose, not appearance. Use "color-surface-primary" instead of "color-white." Use "color-text-muted" instead of "color-gray-500." This semantic naming makes dark mode trivial because "surface-primary" can map to white in light mode and slate-900 in dark mode without renaming anything.

For spacing and typography, custom properties work well at the scale level. Define a spacing scale (space-1 through space-12) and a type scale (text-xs through text-3xl) as custom properties. Components reference the scale values. When you need to adjust density (compact mode vs comfortable mode), you update the scale values on a parent container. Every child component adapts.

Limitations to know

CSS custom properties cannot be used in media queries. You cannot write something like @media (min-width: var(--breakpoint-md)). This is a spec limitation that has not changed. Custom properties also have no type system by default, though the @property rule in modern browsers lets you declare types, initial values, and inheritance behavior. Browser support for @property is strong in 2026 but still requires fallbacks for older environments. Finally, custom properties offer no built-in tooling for cross-platform output. They are CSS-only, which is fine if your product is web-only.

Tailwind Config: Utility-First, Compile-Time, and Opinionated

Tailwind CSS takes a different angle. Your design values live in a configuration file (tailwind.config.js in v3, or directly in your CSS file in v4's CSS-first config). Those values generate utility classes at compile time. Instead of writing custom CSS that references variables, you apply utility classes directly to your markup: bg-brand-primary, text-lg, p-4, rounded-xl. The design system becomes the class vocabulary.

Tailwind v4 introduced a significant shift. Configuration moved from JavaScript into CSS using @theme directives. You now define your design tokens directly in a CSS file, and Tailwind reads them to generate utilities. This is more than a syntax change. It means your Tailwind config values are now CSS custom properties under the hood. When you define a color in @theme, Tailwind generates both the utility class and a corresponding CSS custom property. The line between Tailwind config and CSS custom properties has blurred considerably.

What Tailwind config does well

  • Enforces consistency through constraints. When your spacing scale only has values from 0.5 to 96, developers cannot invent arbitrary pixel values. The utility classes are the guardrails.
  • Collocates style with markup. You see what a component looks like by reading its class names. No jumping between files. No hunting for where a CSS variable is defined.
  • Optimizes output automatically. Tailwind's compiler only emits the CSS for classes you actually use. Dead code elimination is built in.
  • Excellent developer experience. The Tailwind IntelliSense VS Code extension gives you autocomplete for every utility, including your custom theme values. This speeds up development significantly.
  • Dark mode via variant. Tailwind's dark: variant is syntactically clean. You write dark:bg-slate-900 next to bg-white and both states are visible in the same class list.

Where Tailwind config is limited

Tailwind config values are primarily a web concern. There is no built-in pipeline to transform your tailwind.config.js into Android or iOS theme files. If you need multi-platform output, Tailwind config alone is not enough. You need tokens upstream.

Runtime theming is also more constrained. In v3, Tailwind classes were generated at compile time with static values. In v4, because theme values become CSS custom properties, you get some runtime flexibility. But the primary model is still compile-time generation. If you need to swap an entire theme dynamically based on user input or tenant configuration, you are better off driving that through CSS custom properties and letting Tailwind reference them.

The config file can also become unwieldy. Large design systems with hundreds of tokens, semantic aliases, component-specific overrides, and multi-brand configurations can turn a Tailwind config into a maintenance burden. At that scale, the config file is doing the job of a token pipeline without the tooling of one.

Head-to-Head Comparison: Theming, Figma, Dark Mode, and Scale

Choosing between these three approaches requires understanding how they differ across the dimensions that actually matter in production. Here is an honest comparison across six categories.

Multi-platform support

Design tokens win outright. Style Dictionary outputs CSS, SCSS, Less, JavaScript/TypeScript modules, Android XML, iOS Swift, Flutter Dart, and any custom format you define. CSS custom properties are web-only. Tailwind config is web-only. If you ship native mobile apps alongside your web product, tokens are the only approach that gives you a single source of truth across all platforms.

Figma integration

Tokens Studio connects Figma directly to your token JSON files, syncing through Git. This is the tightest design-to-code loop available in 2026. CSS custom properties have no native Figma integration. Designers define values in Figma's own variable system, and engineers manually translate them into CSS. Tailwind has community plugins that attempt to sync Figma variables to Tailwind config, but none are as mature as Tokens Studio. For teams that want designers to push changes that land in code automatically, tokens plus Tokens Studio is the clear winner. Our Figma Dev Mode vs Storybook vs Zeroheight breakdown digs deeper into these design-to-code workflows.

Dark mode

CSS custom properties offer the most elegant dark mode implementation. Define semantic tokens as properties, override them in a dark context, done. Every component adapts without touching its own styles. Tailwind's dark: variant is also effective but requires adding dark-mode classes to every element that changes. For small apps, this is fine. For large apps with hundreds of components, the duplication adds up. Design tokens handle dark mode by defining separate token sets (light and dark) that map to the same token names, then outputting the appropriate values per context.

White-labeling and multi-tenant theming

This is where the approaches diverge most. If each customer gets a unique brand, you need to swap entire theme sets at runtime or build time. Design tokens let you maintain separate JSON files per tenant and run the pipeline to generate tenant-specific outputs. CSS custom properties let you swap themes at runtime by loading a different set of property values. Tailwind config alone struggles here because you would need to rebuild the CSS for each tenant or use CSS custom properties as the underlying values (which is essentially combining approaches). The recommended pattern for white-label: tokens as the source, CSS custom properties as the delivery mechanism, Tailwind utilities referencing those properties for developer ergonomics.

Team reviewing design system architecture and theming strategy on screen

Developer experience

Tailwind wins on day-to-day developer experience. Autocomplete, instant feedback, no context switching between files. CSS custom properties are familiar to any frontend developer but lack the guardrails and tooling of Tailwind. Design tokens require learning a build pipeline (Style Dictionary transforms, formatters, naming conventions), which has a steeper onboarding curve. However, once the pipeline is set up, developers rarely interact with it directly. They reference the generated outputs.

Performance

Tailwind generates minimal CSS because it only includes used utilities. CSS custom properties add a small runtime cost because the browser resolves them at paint time, but this cost is negligible in practice. Design tokens have no runtime cost themselves because they are transformed at build time. The performance differences are marginal for all three approaches in real-world applications.

Recommended Combinations for Real Projects

In practice, you rarely choose just one. The strongest design systems layer these approaches together, with each layer handling what it does best. Here are the combinations I recommend based on project type.

Single web app, one brand, small team

Use Tailwind config as your primary token store. Define your colors, spacing, typography, and shadows directly in your Tailwind theme (in v4, this means your CSS file with @theme). Use CSS custom properties for runtime concerns: dark mode toggle, user-selected accent colors, or reduced-motion preferences. Skip the full token pipeline. The overhead is not justified for a single platform with one brand. You can always add tokens later if requirements change.

This setup gets you shipping fast. Your design values live in one file. Tailwind IntelliSense gives you autocomplete. Dark mode works through CSS custom properties referenced by Tailwind utilities. Total setup time: under an hour.

Multi-platform product (web + iOS + Android)

Start with design tokens in JSON, managed through Tokens Studio for Figma integration. Use Style Dictionary to transform tokens into CSS custom properties for web, XML resources for Android, and Swift asset catalogs for iOS. On the web side, configure Tailwind to reference the generated CSS custom properties so developers still get utility-class ergonomics. This is more infrastructure upfront, but it pays for itself the first time a designer changes a color in Figma and it propagates to all three platforms through CI.

The key architectural decision: your token names are the API. Name them semantically (color.interactive.primary, not color.blue.500). Platform-specific outputs map those semantic names to platform-native constructs. Web gets var(--color-interactive-primary). Android gets @color/colorInteractivePrimary. iOS gets Color.interactivePrimary. Same intent, native expression.

White-label SaaS with tenant theming

This requires all three layers working together. Design tokens store the base theme and each tenant override as separate JSON files. Style Dictionary generates CSS custom property sets per tenant. Your application loads the appropriate property set at runtime based on the tenant context, either through a CSS file swap or by injecting properties on the root element. Tailwind utilities reference those CSS custom properties, so your component code is theme-agnostic. A button is always bg-brand-primary. What "brand-primary" resolves to depends on which tenant's properties are loaded.

For teams building this pattern, I suggest starting with three to five semantic color tokens (brand primary, brand secondary, surface, text, border) and expanding as needed. Over-engineering the token set before you have real tenant requirements leads to unused abstractions. Our guide to AI-powered design system generation covers how to accelerate the component side of this workflow once your tokens are in place.

Design system package consumed by multiple apps

Tokens are the published API. Your design system npm package exports a CSS file containing custom properties generated from your tokens. Consuming apps can override specific properties to customize the theme. If consuming apps use Tailwind, they configure their theme to reference the design system's custom properties. If they use plain CSS, they reference the properties directly. The token layer ensures consistency. The CSS property layer ensures flexibility. The Tailwind layer is optional and depends on the consumer's stack.

Migration Path and Getting Started Today

If you are starting from scratch, the cleanest path is to begin with your Tailwind config (or CSS-first config in v4) and define your design values there. Use semantic names from day one. Call it "brand-primary" instead of "blue-600." This costs nothing extra and saves you significant refactoring later when you add dark mode, theming, or a token pipeline.

If you already have a Tailwind project and need to add runtime theming, introduce CSS custom properties as the values behind your Tailwind theme. In v4, this happens naturally because @theme generates custom properties. In v3, you manually map Tailwind colors to CSS variables in your config. Either way, the migration is incremental. You do not need to rewrite your components. You replace static hex values in your config with custom property references.

If you are adding a native mobile app to an existing web product, that is when you introduce design tokens. Extract your current design values into JSON. Set up Style Dictionary with transforms for web (CSS custom properties) and your mobile platform. Point your Tailwind config at the generated CSS properties. Now you have a pipeline. Changes flow from tokens to all platforms. The web experience does not change for your developers. They still write Tailwind utility classes. But the values behind those classes now come from a shared source.

Tools to evaluate

  • Style Dictionary (v4). The standard token build tool. Handles transforms, formatters, and multi-platform output. Free and open source.
  • Tokens Studio for Figma. Connects Figma variables to your token JSON files via Git. Free tier covers most teams. Pro tier adds multi-file sync and advanced features.
  • Tailwind v4 CSS-first config. Define your theme in CSS with @theme. Generates utilities and custom properties from the same source. No separate config file needed.
  • Cobalt UI. A newer alternative to Style Dictionary with a W3C Design Tokens format focus. Worth evaluating if you are starting fresh and want spec compliance.
  • Specify. A commercial platform that manages tokens with a GUI, syncs with Figma, and outputs to any format. Good for teams that want less build tooling to maintain.

The bottom line

Design tokens, CSS custom properties, and Tailwind config are not competing solutions. They operate at different levels of abstraction. Tokens are the source of truth. CSS custom properties are the runtime delivery mechanism. Tailwind config is the developer interface. The question is not which one to use. The question is how many layers your project actually needs today, and where to leave room for the layers you will need tomorrow.

Start lean. Name things semantically. Add layers when real requirements demand them, not when hypothetical future requirements suggest them. If you want help designing a token architecture for your product or migrating an existing codebase to a layered design system, book a free strategy call and we will map out the right approach for your stack and scale.

Need help building this?

Our team has launched 50+ products for startups and ambitious brands. Let's talk about your project.

design tokensCSS custom propertiesTailwind config themingStyle Dictionary guidedesign system architecture

Ready to build your product?

Book a free 15-minute strategy call. No pitch, just clarity on your next steps.

Get Started