Learn Design Tokens Using CSS Variables and Tailwind Config

Oct 12, 2026AngularEngineeringDesign
Summarize with AI: Google AI Claude ChatGPT Perplexity Grok

AI links open with a title + excerpt (these tools can't fetch the page themselves) — use "Copy full article" to paste the complete text for a fuller summary.

Share:

Introduction

This article demonstrates how to make a design tokens file the real source of truth for a Tailwind project, instead of a document that slowly stops matching the code.

Most teams write the tokens into tailwind.config.js as plain values. That works, but the config is build-time — change a value and you rebuild. We wanted the tokens to be live CSS custom properties, so one file controls the running app.

Design Tokens

A design token is a named design decision. Not #193f7e, but "primary brand colour". Not 16px, but "space-4".

The point is that the name goes in your components and the value lives in one place. Change the value, every component follows.

Features

  • One file holds every spacing, size, colour, radius and shadow.
  • Editing a value changes the live app with no component change.
  • Dark mode is the same tokens with different values.
  • Tailwind utilities keep working exactly as normal.

With the following steps, you can wire it up.

  1. Write the tokens as CSS custom properties.
  2. Point Tailwind at those variables.
  3. Know which tokens cannot be variables.
  4. Add semantic colour keys.
  5. Flip the tokens for dark mode.

Write the Tokens as CSS Custom Properties

frontend/src/tokens.css holds them, imported from styles.css.

:root {
  --tk-space-4: 1rem;
  --tk-text-md: 1rem;
  --tk-bg-base: #f4f6fc;
  --tk-text-primary: #0b1220;
  --tk-border: #e2e8f0;
  --tk-radius-md: 8px;
  --tk-shadow-sm: 0 1px 2px rgb(0 0 0 / 0.06);
}

Plain CSS. No build step, no preprocessor. Every value the design system defines lives here.

Point Tailwind at Those Variables

This is the part most setups skip. In tailwind.config.js, a Tailwind scale can hold a var().

spacing: {
  4: 'var(--tk-space-4)',
  // …
},
fontSize: {
  base: 'var(--tk-text-md)',
  // …
},

Now p-4, gap-4, m-4 and text-base all read from the tokens file at runtime. Every existing utility across every component picks up the change with no edit anywhere.

This was a value-preserving swap for us, which is worth knowing before you try it. Tailwind's default spacing scale is already 4px-based, the same as ours, so replacing the numbers with variables changed nothing visually. If your scale differs from Tailwind's, expect the swap itself to move your layout.

One mismatch we had to map around: our type scale has an extra md step that Tailwind's named scale does not, so Tailwind's keys (xs … 6xl) map one step up into ours. text-7xl, used only by the hero heading, has no equivalent and keeps Tailwind's own value.

One token edit changing spacing across the whole app

Know Which Tokens Cannot Be Variables

Here is the limitation nobody mentions, and you will hit it within an hour.

Tailwind cannot resolve a var() inside a gradient utility or an opacity modifier.

<!-- These do NOT work if the colour is a var() -->
<div class="from-primary-600 to-accent-500">
<div class="bg-primary-500/20">

Tailwind generates those by manipulating the colour value at build time — splitting it into channels for the /20 opacity, or composing gradient stops. It cannot do that to the string var(--tk-primary-600), because at build time it has no idea what colour that is.

So brand colours and radii are copied into the Tailwind scale as static values.

colors: {
  primary: { 600: '#193f7e', /* full 50–950 scale */ },
  accent:  { 500: '#128686', /* … */ },
  gold:    { 400: '#e5a718', /* … */ },
},

That is a split, and splits are usually bad. This one is defensible because the two groups behave differently. Spacing and type change often during design work and are never used in gradients. Brand colours almost never change and are used in gradients constantly.

Be honest about the cost: brand colours now live in two places, the tokens file and the config. If you change one, change both. We accepted it because the alternative was giving up from-primary-600 everywhere.

Add Semantic Colour Keys

The tokens that are not brand colours get their own Tailwind keys, named by role rather than by shade.

Role Tailwind key Token
Surfaces surface-base, surface, surface-raised, surface-sunken --tk-bg-*
Text tk-primary, tk-secondary, tk-muted, tk-faint --tk-text-*
Borders line, line-strong --tk-border*

So components write text-tk-secondary instead of text-gray-600 dark:text-gray-400.

This is the payoff, and it is bigger than it looks. Because the tokens themselves flip in dark mode, a semantic class needs no dark: variant at all. text-tk-secondary is correct in both themes. Every component loses half its colour classes, and no one can forget the dark variant on a new component, because there is no dark variant to forget.

Flip the Tokens for Dark Mode

Dark mode is class-based, so the override is one block.

:root.dark {
  --tk-bg-base: #060d1e;
  --tk-text-primary: #e6edf7;
  --tk-border: #1e293b;
}

Only surfaces, text and borders are redefined. Brand colours and status colours (success, warning, danger, info) stay the same in both themes, because a success green that changes meaning between themes is worse, not better.

The upstream token spec used [data-theme="dark"]. We adapted it to :root.dark to match Tailwind's darkMode: 'class' and the existing toggle. If you adopt someone else's token file, expect to adapt its theming selector to yours — it is a five-minute change and it is much better than changing your whole app to match the file.

The One Deliberate Exception

Our footer copyright line sits on a permanently dark strip, whatever the site theme. So it uses a fixed text-gray-400, not a token that would flip with the toggle.

That is the right call, and it is worth writing down where you make one. A future reader seeing a raw Tailwind grey in a token-based codebase will assume it is a mistake unless you tell them otherwise.

Conclusion

In this article we learned how to define design tokens as CSS custom properties, wire Tailwind's spacing and font-size scales to them with var(), keep brand colours static because gradients and opacity modifiers need real values, expose semantic colour keys that need no dark: variant, and flip the whole theme by redefining a handful of tokens.

The result is a tokens file that is not documentation. It is the running app.

Reference

#design-tokens#tailwind#css-variables#angular#design-system#theming

Comments

Be the first to comment.

Leave a comment

Never shown publicly.

Comments are reviewed before appearing publicly.