OneMinuteBrandingOneMinuteBrandingGenerate Brand
  1. Home
  2. Blog
  3. Tailwind v4 Design Tokens: What Changed and How to Migrate
tailwinddesign tokensmigration

Tailwind v4 Design Tokens: What Changed and How to Migrate

Tailwind v4 changed how design tokens work. CSS-first config, @theme directive, no more tailwind.config.ts. Here's what you need to update.

March 15, 20267 min readBy Yann Lephay

You run npm install tailwindcss@next, delete your 340-line tailwind.config.ts, and watch your dev server crash. Tailwind v4 killed the JavaScript configuration file. Your custom brand colors, spacing scales, and typography settings now live directly in CSS. This is the largest architectural shift since the JIT compiler in v2.1, and it fundamentally changes how you define and consume design tokens.

The Redundant Token Problem

Managing design tokens across JavaScript and CSS has always been a structural failure. You define a custom brand color in tailwind.config.ts to generate utility classes, but when you need that exact hex code for a Recharts component or a custom SVG animation, the JavaScript config traps your data.

You end up writing fragile sync scripts or manually copying #0ea5e9 into a :root CSS block. Keeping theme.extend.colors synchronized with CSS custom properties requires either a third-party plugin or duplicating 11 color shades (50-950) by hand.

Tailwind v4 solves this by making CSS the single source of truth. The framework now ships with a heavily optimized Rust engine that parses your globals.css file, reads native CSS variables, and dynamically generates utilities based on specific naming conventions.

CSS-First Theming with @theme

The @theme directive replaces the theme.extend object. You drop this directly into your main stylesheet. Tailwind scans this block, registers the variables as design tokens, and exposes them to the utility generator.

Here is the exact migration path for a standard brand color scale.

Before (Tailwind v3 - tailwind.config.ts):

Code
export default {
  theme: {
    extend: {
      colors: {
        brand: {
          50: '#f0f9ff',
          100: '#e0f2fe',
          500: '#0ea5e9',
          900: '#0c4a6e',
          950: '#082f49',
        },
      },
      fontFamily: {
        sans: ['Inter', 'sans-serif'],
      },
    },
  },
}
**After (Tailwind v4 - `app.css`):**
```css
@import "tailwindcss";
 
@theme {
  --color-brand-50: #f0f9ff;
  --color-brand-100: #e0f2fe;
  --color-brand-500: #0ea5e9;
  --color-brand-900: #0c4a6e;
  --color-brand-950: #082f49;
  
  --font-sans: "Inter", sans-serif;
}

CSS variables inside @theme are automatically available via the var() function globally, but Tailwind uses the prefix to determine the utility class. The --color- namespace tells the engine to generate bg-brand-500, text-brand-500, and border-brand-500.

Manually mapping hex codes to 11 CSS variables per color family takes an hour of tedious copy-pasting. If you want to bypass this entirely, OneMinuteBranding generates a fully v4-compliant CSS token file. You pay $49 one-time, input your core brand parameters, and 60 seconds later you get a ready-to-paste @theme block with mathematically perfect 50-950 scales for your primary, secondary, and neutral colors.

Migrating Your Brand Tokens

Tailwind v4 enforces strict namespace rules. If you name a variable incorrectly, the compiler ignores it.

v3 Config Keyv4 CSS NamespaceGenerated Class Example
theme.colors--color-*bg-blue-500, text-blue-500
theme.spacing--spacing-*p-4, m-4, gap-4
theme.borderRadius--radius-*rounded-lg, rounded-full
theme.fontFamily--font-*font-sans, font-mono
theme.fontSize--text-*text-sm, text-xl
theme.boxShadow--shadow-*shadow-md, shadow-xl

To migrate spacing scales, you no longer define arbitrary string mappings like 4: '1rem'. You define the base spacing unit, and Tailwind handles the multipliers automatically.

If you need a custom spacing value that breaks the default 0.25rem multiplier grid, you declare it explicitly:

Code
@theme {
  /* Generates p-18, m-18, gap-18 */
  --spacing-18: 4.5rem;
  
  /* Overrides the default large radius */
  --radius-lg: 0.75rem;
}

Handling Dark Mode via CSS Custom Properties

Tailwind v3 relied on the darkMode: 'class' toggle in the configuration file, forcing you to litter your JSX with 400 instances of bg-white dark:bg-gray-900. This approach balloons your HTML payload and makes refactoring base themes a nightmare.

Tailwind v4 pushes you toward semantic CSS variables for theme switching. Instead of declaring literal colors in your markup, you define functional tokens (--color-surface, --color-text-primary) and remap their values based on the color scheme.

This requires moving your token assignments outside of the @theme block and into standard CSS selectors. The @theme block is for defining the raw palette. The :root and .dark selectors are for applying them.

Code
@import "tailwindcss";
 
/* 1. Define the raw palette */
@theme {
  --color-brand-500: #0ea5e9;
  --color-slate-900: #0f172a;
  --color-slate-50: #f8fafc;
}
 
/* 2. Map semantic variables for Light Mode */
:root {
  --color-surface: var(--color-slate-50);
  --color-text-primary: var(--color-slate-900);
  --color-border: var(--color-slate-200);
}
 
/* 3. Re-map for Dark Mode */
@media (prefers-color-scheme: dark) {
  :root {
    --color-surface: var(--color-slate-900);
    --color-text-primary: var(--color-slate-50);
    --color-border: var(--color-slate-700);
  }
}
 
/* Or using a class-based approach */
:root.dark {
  --color-surface: var(--color-slate-900);
  --color-text-primary: var(--color-slate-50);
}

Now your React component drops the dark: prefix entirely. You write <div className="bg-surface text-text-primary border-border">. When the user toggles dark mode, the browser repaints the UI instantly by swapping the underlying CSS variable values.

The End of @apply Abuse

Tailwind v4 severely restricts the @apply directive. In v3, developers used @apply to recreate Bootstrap-style utility classes, fundamentally misunderstanding the utility-first methodology. You would see 50-line .btn-primary classes in global stylesheets compiling down to massive CSS files.

In v4, @apply is restricted to the file where the utilities are imported. You cannot use it inside CSS modules (button.module.css) without explicitly re-importing Tailwind, which duplicates the framework payload.

If you have a component library built on @apply, v4 will break it. The fix is moving component abstractions into your UI layer (React/Vue/Svelte components) or using the tv (Tailwind Variants) library to manage string concatenation in JavaScript. CSS is for tokens, JS is for component logic.

FAQ

Do I need to manually migrate my entire config?

No. Tailwind provides an official upgrade tool: npx @tailwindcss/upgrade@next. You run this in your project root, and it automatically parses your tailwind.config.ts and writes the equivalent @theme CSS variables into your main stylesheet. However, the CLI outputs heavily nested, verbose CSS. Running the tool is a good first step, but you must manually clean up the generated file to maintain a readable token system.

What happens to my third-party Tailwind plugins?

Plugins written for v3 that inject complex JavaScript logic into the theme engine will fail. The v4 architecture drops the Node.js evaluation step during the build process. If you rely on plugins like @tailwindcss/forms or @tailwindcss/typography, you must install their specific v4-compatible alpha versions (npm install @tailwindcss/typography@next). Custom internal plugins should be rewritten as standard CSS using the @utility directive.

Can I still use tailwind.config.ts if I refuse to write CSS?

Yes, using the @config directive in your CSS file (@config "../../tailwind.config.ts";). But you are fighting the compiler. The Rust engine is optimized for CSS variable parsing. Forcing it to bridge to a JavaScript file significantly degrades build performance and locks you out of native CSS variable intellisense in your editor. Delete the JS file.

How do I handle opacity modifiers with CSS variables?

Tailwind v4 handles opacity automatically if your CSS variables contain raw hex codes or standard color functions. When you write bg-brand-500/50, the engine parses --color-brand-500: #0ea5e9; and dynamically injects the alpha channel using the modern color-mix() CSS function. You do not need to define RGB comma-separated values (255, 255, 255) like you did in v2 and v3.

To start the migration, open your terminal, commit your current working tree, and run npx @tailwindcss/upgrade@next. Open your globals.css file, delete the redundant variables the CLI generated, and map your semantic tokens to the new --color-* namespace.

Y
Yann Lephay@YannBuilds

Vibe coder & Indie Hacker. Building tools to help devs ship faster. Creator of OneMinuteBranding.

Ready to create your brand?

Generate a complete brand system with Tailwind config in 60 seconds.

Generate your brand

Related articles

Keeping Brand Consistency Across Multiple Repos

Your marketing site, docs, dashboard, and mobile app all need the same brand. Here's how to share design tokens across repos without losing your mind.

CSS Variables vs Tailwind Config: Where Should Your Brand Live?

Both work for storing brand tokens. But they serve different purposes. Here's when to use CSS custom properties and when to use tailwind.config.ts.

How to Set Up a Design System in 10 Minutes (Without a Designer)

You don't need Figma, a design team, or a 200-page style guide. Here's how to set up a functional design system with Tailwind and CSS variables in under 10 minutes.

Explore more

Branding by RoleBranding by IndustryUse CasesFeaturesIntegrationsGlossaryFree Tools
BlogAboutTermsPrivacy