Tailwind CSS Design Tokens: v4 and v3 Setup Guide (2026)
tailwind-cssdesign-tokenssaascss-custom-propertiesdark-modefigmadesign-systemtheming15 min read

Tailwind CSS Design Tokens: v4 and v3 Setup Guide (2026)

Gaurav Guha•

Every SaaS product starts small. A few screens, one color palette, a handful of components. But as the product grows: more pages, more teams, dark mode, a rebrand. And things drift. Buttons get slightly different padding. Headlines use inconsistent sizes. Someone hard-codes a hex color instead of using the variable. These small inconsistencies compound into a UI that feels unpolished and is expensive to maintain.

Design tokens solve this problem at the root. They give your entire frontend a single source of truth for visual decisions (colors, spacing, typography, radii, shadows), and when those decisions change, you update them in one place. Combined with Tailwind CSS, design tokens create a system that is both developer-friendly and design-consistent.

This guide walks through how to build a practical, production-ready design token system with Tailwind CSS for SaaS products. It covers both versions: Tailwind v4, where tokens live in CSS under @theme, and Tailwind v3, where CSS custom properties are wired in through tailwind.config. If you're starting a new project in 2026, you're on v4, so start with the v4 section below. If you want to skip the setup and start with a token-driven foundation that already works, the SaaS Starter Kit from thefrontkit ships with a complete, production-ready token system.

What Design Tokens Are and Why They Matter

A design token is a named value that represents a visual design decision. Instead of writing #4F46E5 directly in your component, you reference a token like color-primary-600. Instead of 16px for padding, you use a spacing token like space-4.

Tokens typically cover four categories:

  • Color: brand colors, neutrals, semantic colors (success, warning, danger), surface and border colors
  • Spacing: padding, margin, and gap values that follow a consistent scale
  • Typography: font families, sizes, weights, line heights, and letter spacing
  • Shape: border radii and shadow definitions

The value of tokens is not abstraction for its own sake. It is that they create a contract between design and code. When a designer says "use the primary color," that maps to a specific token. When engineering needs to support dark mode, they swap token values rather than rewriting components. When the company rebrands, they update the token layer and every component reflects the change automatically.

For SaaS products specifically, tokens matter because:

  1. Multiple surfaces share the same brand. Your marketing site, dashboard, settings pages, onboarding flows, and email templates all need to feel like the same product.
  2. Teams grow. Without tokens, each developer interprets "the blue" slightly differently.
  3. Theming is expected. Dark mode is table stakes. White-labeling is common. Both require a token layer.
  4. Design-to-code handoff is smoother. Tokens give designers and developers a shared vocabulary that eliminates ambiguity.

For a deeper look at why consistency matters for SaaS and AI products, see how design tokens keep your SaaS UI cohesive.

Tailwind v4: Define Tokens with @theme

Tailwind v4 moved configuration into CSS. Instead of a tailwind.config.ts file, you declare design tokens as theme variables inside an @theme block, and Tailwind generates the matching utility classes from them.

/* globals.css */
@import "tailwindcss";

@theme {
  --color-primary-50: oklch(0.96 0.02 272);
  --color-primary-600: oklch(0.51 0.23 277);
  --color-primary-700: oklch(0.46 0.21 277);

  --font-sans: "Inter", system-ui, sans-serif;
  --text-xs: 0.75rem;
  --text-sm: 0.875rem;

  --spacing: 0.25rem;

  --radius-md: 0.5rem;
  --radius-lg: 0.75rem;

  --shadow-card: 0 1px 2px 0 rgb(0 0 0 / 0.05);
}

That's the whole setup. --color-primary-600 gives you bg-primary-600, text-primary-600 and border-primary-600. --radius-lg gives you rounded-lg, and --shadow-card gives you shadow-card. Tailwind also outputs every theme variable as a regular CSS variable on :root, so you can use var(--color-primary-600) in plain CSS or inline styles.

The prefix decides which utilities a token creates. The main namespaces:

Theme variable Creates utilities like
--color-* bg-*, text-*, border-*, fill-*
--font-* font-sans, font-mono
--text-* text-sm, text-lg (font size)
--font-weight-* font-medium, font-bold
--leading-* / --tracking-* line height and letter spacing
--spacing p-4, gap-6, mt-2 (multiples of the base)
--radius-* rounded-*
--shadow-* shadow-*
--breakpoint-* responsive variants like md:

Three v4 details that matter for a token system:

  • Spacing is one token. v4 uses a single --spacing base value, and p-4 compiles to calc(var(--spacing) * 4). Change --spacing and your whole spacing scale moves with it.
  • No more raw RGB channels. v3 needed colors stored as 79 70 229 so opacity modifiers would work. v4 handles bg-primary-600/50 with color-mix(), so you can store colors in any format. OKLCH is the default in v4's own palette.
  • You can clear the defaults. --color-*: initial; inside @theme removes Tailwind's built-in colors, so the only colors available are your tokens. That's the easiest way to stop people reaching for bg-blue-500.

Semantic tokens in v4 with @theme inline

For theming and dark mode, you want components to use role-based tokens like bg-surface and text-muted while the actual values switch per theme. Define the raw values as normal CSS variables, then map them into Tailwind with @theme inline:

:root {
  --surface: oklch(1 0 0);
  --text: oklch(0.21 0.03 265);
  --muted: oklch(0.55 0.02 265);
  --border: oklch(0.92 0.01 265);
}

.dark {
  --surface: oklch(0.21 0.03 265);
  --text: oklch(0.98 0 0);
  --muted: oklch(0.71 0.02 265);
  --border: oklch(0.37 0.03 265);
}

@theme inline {
  --color-surface: var(--surface);
  --color-text: var(--text);
  --color-muted: var(--muted);
  --color-border: var(--border);
}

@custom-variant dark (&:where(.dark, .dark *));

inline tells Tailwind to use the variable reference directly in each utility, so bg-surface resolves to whatever --surface is in the current theme. The @custom-variant line makes the dark: variant follow a .dark class instead of the operating system setting. This is the same pattern shadcn/ui uses for its themes.

Still have a tailwind.config file?

v4 can still load a JavaScript config with @config "./tailwind.config.ts"; in your CSS, which is useful while migrating. For new projects, @theme is simpler: tokens and utilities live in one file, and there's no build step between them.

Tailwind v3: CSS Custom Properties + tailwind.config

On Tailwind v3, tokens live as CSS custom properties and the config file maps them to utilities.

CSS custom properties (also called CSS variables) are the foundation of a modern token system. They let you define values once and reference them everywhere, including from within Tailwind's configuration.

Here is the core pattern. You define your tokens as CSS custom properties in your global stylesheet:

/* globals.css */
:root {
  --color-primary-50: 238 242 255;
  --color-primary-100: 224 231 255;
  --color-primary-500: 99 102 241;
  --color-primary-600: 79 70 229;
  --color-primary-700: 67 56 202;

  --color-neutral-50: 249 250 251;
  --color-neutral-200: 229 231 235;
  --color-neutral-700: 55 65 81;
  --color-neutral-900: 17 24 39;

  --color-success-500: 34 197 94;
  --color-danger-500: 239 68 68;

  --radius-sm: 0.25rem;
  --radius-md: 0.5rem;
  --radius-lg: 0.75rem;
  --radius-xl: 1rem;

  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-6: 1.5rem;
  --space-8: 2rem;
}

Then you wire these properties into your Tailwind config so they become available as utility classes:

// tailwind.config.ts
import type { Config } from "tailwindcss"

const config: Config = {
  theme: {
    extend: {
      colors: {
        primary: {
          50: "rgb(var(--color-primary-50) / <alpha-value>)",
          100: "rgb(var(--color-primary-100) / <alpha-value>)",
          500: "rgb(var(--color-primary-500) / <alpha-value>)",
          600: "rgb(var(--color-primary-600) / <alpha-value>)",
          700: "rgb(var(--color-primary-700) / <alpha-value>)",
        },
        neutral: {
          50: "rgb(var(--color-neutral-50) / <alpha-value>)",
          200: "rgb(var(--color-neutral-200) / <alpha-value>)",
          700: "rgb(var(--color-neutral-700) / <alpha-value>)",
          900: "rgb(var(--color-neutral-900) / <alpha-value>)",
        },
        success: {
          500: "rgb(var(--color-success-500) / <alpha-value>)",
        },
        danger: {
          500: "rgb(var(--color-danger-500) / <alpha-value>)",
        },
      },
      borderRadius: {
        sm: "var(--radius-sm)",
        md: "var(--radius-md)",
        lg: "var(--radius-lg)",
        xl: "var(--radius-xl)",
      },
    },
  },
}

export default config

Now you can write bg-primary-600 or rounded-lg in your components, and the actual values come from your CSS custom properties. This is the key insight: Tailwind becomes the utility layer, and CSS custom properties become the token layer. The two work together without either one replacing the other.

Notice that color values are stored as raw RGB channels (79 70 229 instead of rgb(79, 70, 229)). This lets v3's opacity modifier syntax work correctly: bg-primary-600/50 will apply 50% opacity without any extra setup.

Setting Up a Complete Token System

A production token system for SaaS needs more than just colors. Here is a complete setup covering the four core categories. The examples use plain :root custom properties, which works on either version: on v3 you map them in tailwind.config, and on v4 you put the same names inside @theme (or map them with @theme inline).

Color Tokens

Structure your colors into three layers:

  1. Brand colors: primary and secondary palettes with a full scale (50 through 900)
  2. Neutral colors: grays used for text, borders, backgrounds, and surfaces
  3. Semantic colors: success, warning, danger, and info, each with a small scale
:root {
  /* Brand */
  --color-primary-50: 238 242 255;
  --color-primary-600: 79 70 229;
  --color-secondary-100: 236 254 255;
  --color-secondary-600: 8 145 178;

  /* Neutral */
  --color-neutral-50: 249 250 251;
  --color-neutral-100: 243 244 246;
  --color-neutral-200: 229 231 235;
  --color-neutral-400: 156 163 175;
  --color-neutral-600: 75 85 99;
  --color-neutral-800: 31 41 55;
  --color-neutral-900: 17 24 39;

  /* Semantic */
  --color-success-100: 220 252 231;
  --color-success-600: 22 163 74;
  --color-warning-100: 254 249 195;
  --color-warning-600: 202 138 4;
  --color-danger-100: 254 226 226;
  --color-danger-600: 220 38 38;

  /* Surfaces */
  --color-surface: 255 255 255;
  --color-surface-raised: 249 250 251;
  --color-border: 229 231 235;
}

Spacing Tokens

Use a consistent scale. Most SaaS products work well with a 4px base unit:

:root {
  --space-0: 0;
  --space-1: 0.25rem;   /* 4px */
  --space-2: 0.5rem;    /* 8px */
  --space-3: 0.75rem;   /* 12px */
  --space-4: 1rem;      /* 16px */
  --space-5: 1.25rem;   /* 20px */
  --space-6: 1.5rem;    /* 24px */
  --space-8: 2rem;      /* 32px */
  --space-10: 2.5rem;   /* 40px */
  --space-12: 3rem;     /* 48px */
  --space-16: 4rem;     /* 64px */
}

Typography Tokens

Define font sizes, weights, and line heights as tokens. This keeps your type hierarchy consistent across every page:

:root {
  --font-family-sans: 'Inter', system-ui, -apple-system, sans-serif;
  --font-family-mono: 'JetBrains Mono', 'Fira Code', monospace;

  --font-size-xs: 0.75rem;
  --font-size-sm: 0.875rem;
  --font-size-base: 1rem;
  --font-size-lg: 1.125rem;
  --font-size-xl: 1.25rem;
  --font-size-2xl: 1.5rem;
  --font-size-3xl: 1.875rem;

  --font-weight-normal: 400;
  --font-weight-medium: 500;
  --font-weight-semibold: 600;
  --font-weight-bold: 700;

  --line-height-tight: 1.25;
  --line-height-normal: 1.5;
  --line-height-relaxed: 1.75;
}

Radius and Shadow Tokens

Radii and shadows define the visual "feel" of your product, sharp and precise or soft and approachable:

:root {
  --radius-none: 0;
  --radius-sm: 0.25rem;
  --radius-md: 0.5rem;
  --radius-lg: 0.75rem;
  --radius-xl: 1rem;
  --radius-full: 9999px;

  --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
  --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
  --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1);
}

Connecting Design Tools to Tailwind Tokens

A token system is only as strong as the connection between your design specifications and your code. If your design tools use different names or values than your Tailwind config, you will spend time translating instead of building.

The approach that works best is to establish a shared naming convention that both design specs and code follow:

  1. Name tokens identically. If your design spec has a variable called color/primary/600, your CSS property should be --color-primary-600. If your design uses radius/md, your code uses --radius-md.

  2. Use CSS custom properties as the source of truth. Define all tokens in a central globals.css file. This becomes the single source of truth that your Tailwind config references and that designers can review directly.

  3. Automate token generation. Scripts can transform token definition files (JSON or CSS) into the formats your build pipeline needs. The workflow looks like this:

Token definitions --> Build script --> globals.css

A basic transformation script might look like:

// scripts/generate-tokens.js
const tokens = require("./tokens.json")

function generateCSS(tokens) {
  let css = ":root {\n"
  for (const [category, values] of Object.entries(tokens)) {
    for (const [name, value] of Object.entries(values)) {
      css += `  --${category}-${name}: ${value};\n`
    }
  }
  css += "}\n"
  return css
}
  1. Validate parity in CI. Add a check to your pipeline that compares the tokens used in your design definitions against the tokens defined in your CSS. If a token is added in one place but missing in another, the build warns you.

This workflow eliminates the "design says one thing, code does another" problem that plagues most SaaS teams. The Tailwind SaaS Template from thefrontkit ships with this architecture built in, where every token maps directly to a CSS custom property consumed by Tailwind utilities.

Token-Driven Dark Mode Implementation

Dark mode is where a token system pays for itself. Without tokens, dark mode means adding conditional classes throughout your codebase. With tokens, dark mode is a single set of variable overrides.

Define your dark mode tokens as overrides on a .dark class or a data-theme="dark" attribute:

:root {
  --color-surface: 255 255 255;
  --color-surface-raised: 249 250 251;
  --color-text-primary: 17 24 39;
  --color-text-secondary: 75 85 99;
  --color-border: 229 231 235;
}

.dark {
  --color-surface: 17 24 39;
  --color-surface-raised: 31 41 55;
  --color-text-primary: 249 250 251;
  --color-text-secondary: 156 163 175;
  --color-border: 55 65 81;
}

Because your components use token-based Tailwind classes like bg-surface and text-primary, toggling dark mode requires zero changes to component code. You toggle a class on the <html> element and every component updates automatically:

// Theme toggle: the only dark mode code you need in components
function toggleTheme() {
  document.documentElement.classList.toggle("dark")
}

This approach also extends cleanly to other themes. If you need a high-contrast mode, a brand-specific theme for white-labeling, or a reduced-motion preference, you add another set of token overrides. The component layer never changes.

The key is using semantic token names rather than literal ones. Name tokens by their role (surface, text-primary, border) rather than their value (white, gray-900). Literal names break down the moment you have more than one theme, because white is not white in dark mode.

How thefrontkit Uses Design Tokens

The SaaS Starter Kit is built entirely on the token-driven architecture described in this guide. Every component (buttons, inputs, sidebars, cards, tables, settings pages) consumes tokens through Tailwind utilities. No component contains a hard-coded color, radius, or spacing value.

Here is how the system works in practice:

Centralized token architecture. All design tokens live in globals.css as CSS custom properties. The Tailwind config references these properties, so every component in the system consumes the same values. Rebranding means updating token values in one file.

Semantic color roles. Colors are organized by role, not by value. The token --color-primary-600 is used for primary actions (buttons, links, active states). The token --color-danger-600 is used for destructive actions and error states. This makes it impossible to accidentally use the "wrong blue" because there is only one token for each purpose.

Theme switching without component changes. Light and dark themes are defined as two sets of token overrides. Components use semantic tokens like bg-surface and text-primary, so they render correctly in both themes without conditional logic.

Consistent spacing across layouts. The sidebar, topbar, content area, cards, and form elements all use the same spacing scale. This creates visual rhythm without manual alignment.

For teams that need production-ready components without building the token system from scratch, the SaaS Starter Kit gives you the complete foundation, with tokens, Tailwind config, and 40+ components that consume them. The same architecture powers the CRM Dashboard App and Next.js Storefront App.

Benefits of a Token-Driven Architecture

Building a token system requires upfront investment. Here is what you get in return.

Consistency Without Policing

When every component pulls from the same token set, UI drift stops happening. You do not need code reviews that catch "wrong shade of blue" or "padding should be 16px not 14px." The system enforces consistency automatically.

Rebrandability

A rebrand with tokens means updating a single CSS file. Without tokens, it means touching every component, every page, every hardcoded value. For SaaS products that white-label or evolve their brand over time, this is the difference between a one-day task and a multi-sprint project.

Team Alignment

Tokens give designers and developers a shared language. When a designer says "use primary-600," there is no ambiguity about what that means in code. This reduces back-and-forth during handoff and keeps the design review cycle short.

Faster Dark Mode and Theming

As covered above, dark mode with tokens is a set of variable overrides. Without tokens, it is a project. The same applies to any future theme requirements: high contrast, white-label, seasonal.

Scalability

As your SaaS grows from 5 screens to 50, tokens keep the UI layer manageable. New pages and features automatically inherit the established visual language. New team members can ship consistent UI from day one because the system guides their choices.

Smaller CSS Bundles

When Tailwind utilities reference CSS custom properties, you get a smaller set of generated classes. Instead of creating new utility classes for every color variation, Tailwind reuses the same class names and the browser resolves the custom property values at runtime.

Frequently Asked Questions

What are design tokens in Tailwind CSS?

Design tokens are named values for design decisions like colors, spacing, font sizes, radii and shadows. In Tailwind v4 you define them as theme variables in an @theme block (for example --color-primary-600), and Tailwind generates utilities like bg-primary-600 from them. In v3 you define them as CSS custom properties and reference them in tailwind.config.

Does Tailwind v4 still use tailwind.config.js?

Not by default. v4 is configured in CSS with @theme. You can still load a JavaScript config with the @config directive, which helps during a migration, but new projects don't need one.

What are semantic tokens in Tailwind?

Semantic tokens are named by role rather than value: surface, text, muted, border, danger. Components use utilities like bg-surface, and each theme sets what surface means. In v4 you map them with @theme inline { --color-surface: var(--surface); }.

How do spacing tokens work in Tailwind v4?

v4 uses one base token, --spacing (0.25rem by default). Every spacing utility is a multiple of it, so p-4 is calc(var(--spacing) * 4). Changing the base value rescales all padding, margin and gap utilities at once.

How do I add dark mode with Tailwind design tokens?

Give each theme its own values for the same CSS variables (:root for light, .dark for dark), map them into Tailwind as semantic tokens, and toggle the dark class on <html>. On v4, add @custom-variant dark (&:where(.dark, .dark *)); so the dark: variant follows the class. Components don't change at all.

Getting Started

If you are starting a new SaaS project, the fastest path is to begin with a foundation that already has tokens, Tailwind config, and components wired together. The SaaS Starter Kit and the Tailwind SaaS Template both provide this out of the box, with a production-ready token architecture and WCAG AA accessibility.

If you are adding tokens to an existing project, start with colors. Map your current color values to CSS custom properties, update your Tailwind config to reference them, and then gradually migrate components from hard-coded values to token-based utilities. Spacing and typography can follow in subsequent passes.

Either way, the investment pays off quickly. A token-driven Tailwind setup gives you consistency, rebrandability, and theming for every component you build from that point forward.

Gaurav Guha, Founder of thefrontkit

Gaurav Guha

Founder, thefrontkit

Building production-ready frontend kits for SaaS and AI products. Previously co-created NativeBase (100K+ weekly npm downloads). Also does AI automation and CRM integration work for clients. Writes about accessible UI architecture, design tokens, and shipping faster with Next.js.

Related Posts

Next.js SaaS Template

Dashboard, auth screens, settings, and 50+ accessible components.