Design Tokens That Scale in 2026 (Tailwind v4 + CSS Variables)
If your UI needs to evolve without breaking, you need tokens. Here's how to structure tokens with Tailwind v4's CSS-first approach and keep animations, spacing, and surfaces consistent at scale.
TL;DR
- Tokens prevent style drift and make global changes safe — they’re essential for scaling UIs
- Tailwind v4 uses a CSS-first approach: all design tokens are exposed as native CSS variables
- Use three token layers: base (raw values), semantic (purpose-driven), component (variants)
- The
@themedirective creates tokens that automatically generate utility classes - Define motion tokens for consistent animation across your site
- Tailwind v4 delivers 5x faster builds and 100x faster incremental builds
Why Design Tokens Matter
Design tokens are the foundational values that define your design system: colors, spacing, typography, motion, and more. Without them, you get:
| Problem | What Happens |
|---|---|
| Style drift | Random hex codes, one-off spacing values |
| Refactor risk | Changing one color breaks unknown components |
| Inconsistency | Same UI element looks different in different places |
| Onboarding friction | New devs don’t know which values to use |
| Theming difficulty | Dark mode becomes a massive undertaking |
What Tokens Give You
| Benefit | How |
|---|---|
| Single source of truth | Change once, update everywhere |
| Safe refactoring | Modify semantic tokens without touching components |
| Consistency | All components use the same values |
| Theming | Swap token values for dark mode, brands, etc. |
| Communication | Designers and devs speak the same language |
Tailwind v4: CSS-First Design Tokens
Tailwind CSS v4 fundamentally changed how design tokens work by moving from JavaScript configuration to a CSS-first approach.
The Key Changes
| Tailwind v3 | Tailwind v4 |
|---|---|
JavaScript tailwind.config.js | CSS @theme directive |
| Compile-time only | Runtime CSS variables |
| Extended via JS objects | Extended via CSS |
| Manual theming setup | Native CSS variable theming |
How @theme Works
@import "tailwindcss";
@theme {
/* These generate utility classes automatically */
--color-primary-500: oklch(59.59% 0.24 255);
--color-primary-600: oklch(49.59% 0.24 255);
--spacing-xs: 0.25rem;
--spacing-sm: 0.5rem;
--spacing-md: 1rem;
--font-display: "Inter", sans-serif;
}
Defining --color-primary-500 automatically generates:
bg-primary-500text-primary-500border-primary-500fill-primary-500- etc.
Performance Gains
Tailwind v4’s new engine delivers:
| Metric | Improvement |
|---|---|
| Full builds | 5x faster |
| Incremental builds | 100x+ faster |
| HMR | Near-instant |
The Token Hierarchy
Structure your tokens in three layers for maximum flexibility and safety.
Layer 1: Base Tokens (Primitives)
Raw values with no semantic meaning. These are your palette.
@theme {
/* Color primitives (OKLCH for perceptual evenness) */
--color-blue-100: oklch(95% 0.02 250);
--color-blue-200: oklch(90% 0.04 250);
--color-blue-300: oklch(80% 0.08 250);
--color-blue-400: oklch(70% 0.12 250);
--color-blue-500: oklch(60% 0.16 250);
--color-blue-600: oklch(50% 0.16 250);
--color-blue-700: oklch(40% 0.14 250);
--color-blue-800: oklch(30% 0.12 250);
--color-blue-900: oklch(20% 0.10 250);
/* Spacing primitives */
--spacing-1: 0.25rem;
--spacing-2: 0.5rem;
--spacing-3: 0.75rem;
--spacing-4: 1rem;
--spacing-6: 1.5rem;
--spacing-8: 2rem;
--spacing-12: 3rem;
--spacing-16: 4rem;
/* Radius primitives */
--radius-sm: 0.25rem;
--radius-md: 0.5rem;
--radius-lg: 1rem;
--radius-full: 9999px;
}
Layer 2: Semantic Tokens (Purpose-Driven)
Tokens that express intent, not raw values. These are what components should use.
@theme {
/* Semantic colors */
--color-background: var(--color-gray-50);
--color-foreground: var(--color-gray-900);
--color-muted: var(--color-gray-500);
--color-border: var(--color-gray-200);
--color-ring: var(--color-blue-500);
/* Semantic surfaces */
--color-surface-primary: var(--color-white);
--color-surface-secondary: var(--color-gray-100);
--color-surface-elevated: var(--color-white);
/* Semantic actions */
--color-action-primary: var(--color-blue-600);
--color-action-primary-hover: var(--color-blue-700);
--color-action-destructive: var(--color-red-600);
/* Semantic states */
--color-success: var(--color-green-600);
--color-warning: var(--color-amber-500);
--color-error: var(--color-red-600);
--color-info: var(--color-blue-500);
/* Semantic spacing */
--space-content: var(--spacing-4);
--space-section: var(--spacing-12);
--space-page: var(--spacing-16);
}
Layer 3: Component Tokens
Tokens specific to component variants.
@theme {
/* Button tokens */
--button-radius: var(--radius-md);
--button-padding-x: var(--spacing-4);
--button-padding-y: var(--spacing-2);
/* Card tokens */
--card-radius: var(--radius-lg);
--card-padding: var(--spacing-6);
--card-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
/* Input tokens */
--input-radius: var(--radius-md);
--input-border-width: 1px;
--input-padding-x: var(--spacing-3);
--input-padding-y: var(--spacing-2);
}
Never skip semantic tokens — they’re what makes refactors safe. Component tokens should reference semantic tokens, semantic tokens reference base tokens.
Motion Tokens (The Missing Piece)
Motion is often overlooked in token systems, leading to inconsistent animation throughout the UI.
The Motion Token Set
@theme {
/* Duration scale */
--duration-instant: 0ms;
--duration-fast: 100ms;
--duration-normal: 200ms;
--duration-slow: 300ms;
--duration-slower: 500ms;
/* Easing curves */
--ease-in: cubic-bezier(0.4, 0, 1, 1);
--ease-out: cubic-bezier(0, 0, 0.2, 1);
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
--ease-bounce: cubic-bezier(0.34, 1.56, 0.64, 1);
--ease-spring: cubic-bezier(0.175, 0.885, 0.32, 1.275);
/* Stagger delays */
--stagger-base: 50ms;
--stagger-fast: 30ms;
--stagger-slow: 100ms;
}
Using Motion Tokens
.button {
transition: all var(--duration-normal) var(--ease-out);
}
.dropdown-item {
transition: background-color var(--duration-fast) var(--ease-in-out);
}
.modal {
transition:
opacity var(--duration-slow) var(--ease-out),
transform var(--duration-slow) var(--ease-spring);
}
Motion Token Guidelines
| Element | Duration | Easing |
|---|---|---|
| Hover states | fast (100ms) | ease-out |
| Button clicks | normal (200ms) | ease-out |
| Tooltips | fast (100ms) | ease-out |
| Dropdowns | normal (200ms) | ease-out |
| Modals | slow (300ms) | ease-spring |
| Page transitions | slower (500ms) | ease-in-out |
OKLCH Color System
Tailwind v4 uses OKLCH colors for perceptually even steps.
Why OKLCH
| Color Space | Issue |
|---|---|
| Hex/RGB | Steps aren’t perceptually even |
| HSL | Same problem |
| OKLCH | Perceptually uniform lightness |
OKLCH Structure
/* oklch(lightness chroma hue) */
--color-blue-500: oklch(60% 0.16 250);
/* │ │ │
│ │ └── Hue (color wheel position)
│ └── Chroma (saturation)
└── Lightness (0% = black, 100% = white)
*/
Creating Even Color Scales
@theme {
/* Even lightness steps for consistent UI */
--color-brand-50: oklch(98% 0.01 250);
--color-brand-100: oklch(95% 0.02 250);
--color-brand-200: oklch(90% 0.05 250);
--color-brand-300: oklch(80% 0.08 250);
--color-brand-400: oklch(70% 0.12 250);
--color-brand-500: oklch(60% 0.16 250); /* Primary */
--color-brand-600: oklch(50% 0.14 250);
--color-brand-700: oklch(40% 0.12 250);
--color-brand-800: oklch(30% 0.10 250);
--color-brand-900: oklch(20% 0.08 250);
--color-brand-950: oklch(12% 0.06 250);
}
Theming with CSS Variables
Tailwind v4’s CSS-variable approach makes theming straightforward.
Dark Mode Theming
@theme {
/* Light mode defaults */
--color-background: var(--color-white);
--color-foreground: var(--color-gray-900);
--color-muted: var(--color-gray-500);
--color-border: var(--color-gray-200);
--color-surface: var(--color-white);
}
/* Dark mode overrides */
@media (prefers-color-scheme: dark) {
:root {
--color-background: var(--color-gray-950);
--color-foreground: var(--color-gray-50);
--color-muted: var(--color-gray-400);
--color-border: var(--color-gray-800);
--color-surface: var(--color-gray-900);
}
}
Brand/White-Label Theming
/* Default brand */
:root {
--color-action-primary: var(--color-blue-600);
--color-action-primary-hover: var(--color-blue-700);
}
/* Client A brand */
[data-brand="client-a"] {
--color-action-primary: var(--color-green-600);
--color-action-primary-hover: var(--color-green-700);
}
/* Client B brand */
[data-brand="client-b"] {
--color-action-primary: var(--color-purple-600);
--color-action-primary-hover: var(--color-purple-700);
}
Runtime Theme Switching
Because tokens are CSS variables, you can change them at runtime:
// Switch to dark mode
document.documentElement.style.setProperty('--color-background', 'oklch(10% 0 0)');
// Or toggle a class
document.documentElement.classList.toggle('dark');
The Drift Checklist
Style drift kills design systems. Watch for these warning signs:
Signs of Drift
| Warning Sign | What’s Happening |
|---|---|
| Random spacing values | pl-[17px] instead of pl-4 |
| Custom shadows everywhere | shadow-[...] proliferating |
| One-off border radii | rounded-[7px] instead of rounded-md |
| Hardcoded colors | #3b82f6 instead of text-primary-500 |
| Inconsistent motion | Every component has different timing |
Prevention Strategies
| Strategy | Implementation |
|---|---|
| ESLint rules | Ban arbitrary values in commits |
| Design review | Check new components against tokens |
| Token audits | Quarterly scan for token violations |
| Documentation | Clear guidance on which token to use |
| Code review focus | Reviewers flag token violations |
Fixing Drift
- Audit for arbitrary values
- Map arbitrary values to nearest token
- Create new tokens only if truly needed
- Update components to use tokens
- Add lint rules to prevent recurrence
Token Organization Best Practices
File Structure
src/styles/
├── tokens/
│ ├── colors.css # Base color primitives
│ ├── spacing.css # Spacing scale
│ ├── typography.css # Font families, sizes
│ ├── motion.css # Duration, easing
│ ├── radius.css # Border radii
│ └── shadows.css # Shadow scale
├── semantic/
│ ├── colors.css # Semantic color mapping
│ └── spacing.css # Semantic spacing
├── components/
│ ├── button.css # Component tokens
│ └── card.css
└── app.css # Main import
Main Import
/* app.css */
@import "tailwindcss";
@import "./tokens/colors.css";
@import "./tokens/spacing.css";
@import "./tokens/typography.css";
@import "./tokens/motion.css";
@import "./tokens/radius.css";
@import "./tokens/shadows.css";
@import "./semantic/colors.css";
@import "./semantic/spacing.css";
@import "./components/button.css";
@import "./components/card.css";
Implementation Checklist
Foundation:
- Define base color palette (OKLCH recommended)
- Define spacing scale
- Define typography scale
- Define border-radius scale
- Define shadow scale
Semantic layer:
- Map colors to semantic names (background, foreground, etc.)
- Define surface colors
- Define action colors
- Define state colors (success, error, etc.)
- Define semantic spacing
Motion:
- Define duration scale
- Define easing curves
- Define stagger delays
- Document motion guidelines
Theming:
- Set up dark mode variables
- Test dark mode contrast
- Set up brand theming (if needed)
Prevention:
- Add ESLint rules for arbitrary values
- Create token documentation
- Set up code review guidelines
- Schedule quarterly audits
FAQ
Do tokens slow down design iteration?
They speed it up once you have more than a handful of components. Initial setup takes time, but changes become trivial afterward. Changing a button color from “update 47 files” to “update one token.”
Should I use CSS variables or Tailwind classes?
Both. Tailwind v4 generates utility classes from your CSS variable tokens. Use utilities for one-off styling; use tokens for systematic values.
How many color steps do I need?
For most applications: 11 steps (50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950). You can start with fewer and add as needed.
What about legacy browsers?
CSS variables (custom properties) are supported in all modern browsers. For legacy support (IE11), you’ll need a fallback strategy or polyfill.
How do I share tokens with designers?
Export tokens to Figma via:
- Design token plugins (Tokens Studio)
- Manual sync with naming conventions
- Token files in shared repo
Should component tokens be global or scoped?
Global tokens should be defined in @theme. Component-specific tokens can be scoped to the component file but should still reference semantic tokens.
Sources & Further Reading
Interested in our research?
We share our work openly. If you'd like to collaborate or discuss ideas — we'd love to hear from you.
Get in Touch