Back to all articles
TechnicalFeature

Tailwind CSS v4: What Changed and Why It's Better

No more tailwind.config.ts. Tailwind v4 moves configuration to CSS, drops JavaScript, and ships 2x faster. Here's everything that changed.

Tailwind CSS v4: What Changed and Why It's Better

Tailwind CSS v4 is a complete rewrite. Not a minor version bump - a fundamental rethinking of how the framework works.

The biggest change? No more tailwind.config.ts. Configuration now lives in CSS.

In this post, I'll walk you through everything that changed in Tailwind v4, why these changes matter, and how we've set it up in Vibestacks.

The Big Picture

Here's what's different in v4:

FeatureTailwind v3Tailwind v4
Configurationtailwind.config.ts (JavaScript)@theme in CSS
Build toolPostCSS pluginNew @tailwindcss/postcss
PerformanceFast40-60% faster
CSS variablesOptionalNative, first-class
Dark modedarkMode: 'class' config@custom-variant dark
Content detectionManual content arrayAutomatic

Let's break down each change.

No More JavaScript Config

In Tailwind v3, you'd configure everything in tailwind.config.ts:

tailwind.config.ts (v3)
import type { Config } from 'tailwindcss'

const config: Config = {
  content: ['./app/**/*.{ts,tsx}', './components/**/*.{ts,tsx}'],
  darkMode: 'class',
  theme: {
    extend: {
      colors: {
        brand: '#3b82f6',
        'brand-dark': '#1d4ed8',
      },
      animation: {
        'spin-slow': 'spin 3s linear infinite',
      },
    },
  },
  plugins: [],
}

export default config

In v4, all of this moves to CSS:

app/globals.css (v4)
@import "tailwindcss";

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

@theme inline {
  --color-brand: #3b82f6;
  --color-brand-dark: #1d4ed8;
  --animate-spin-slow: spin 3s linear infinite;
}

Why is this better?

  1. No JavaScript in your styling - CSS stays in CSS
  2. IDE support - CSS syntax highlighting and autocomplete work everywhere
  3. Faster builds - No JavaScript parsing or execution
  4. Simpler mental model - Everything style-related is in one place

CSS Variables Are Now Native

In v3, using CSS variables required extra setup. In v4, they're the default.

When you define a color in @theme:

@theme inline {
  --color-primary: oklch(0.205 0 0);
}

Tailwind automatically generates:

  • The utility class bg-primary, text-primary, etc.
  • A CSS variable --color-primary you can use anywhere

This means you can do things like:

.custom-element {
  /* Use Tailwind's variable directly */
  background: var(--color-primary);
  
  /* Or with opacity */
  background: oklch(from var(--color-primary) l c h / 50%);
}

The tight integration between Tailwind utilities and CSS variables makes theming dramatically simpler.

The New @theme Directive

The @theme block is where you define your design tokens:

@theme inline {
  /* Colors */
  --color-background: var(--background);
  --color-foreground: var(--foreground);
  --color-primary: var(--primary);
  --color-primary-foreground: var(--primary-foreground);
  
  /* Typography */
  --font-sans: var(--font-geist-sans);
  --font-mono: var(--font-geist-mono);
  
  /* Spacing & Sizing */
  --radius-sm: calc(var(--radius) - 4px);
  --radius-md: calc(var(--radius) - 2px);
  --radius-lg: var(--radius);
  
  /* Animations */
  --animate-fade-in: fade-in 0.2s ease-out;
  
  @keyframes fade-in {
    from { opacity: 0; }
    to { opacity: 1; }
  }
}

The inline keyword tells Tailwind to output these variables directly in your CSS, making them available to your entire app.

Naming Conventions

Tailwind v4 uses a prefix-based naming system:

PrefixGeneratesExample
--color-*Color utilitiesbg-brand, text-brand
--font-*Font family utilitiesfont-sans, font-mono
--animate-*Animation utilitiesanimate-fade-in
--radius-*Border radius utilitiesrounded-lg
--spacing-*Spacing utilitiesCustom spacing scale

Define a variable with the right prefix, and Tailwind handles the rest.

Dark Mode: @custom-variant

In v3, dark mode required a config option:

v3
module.exports = {
  darkMode: 'class',
  // ...
}

In v4, it's a CSS directive:

v4
@custom-variant dark (&:is(.dark *));

This line says: "The dark: variant should match elements inside a .dark parent."

You can also define other custom variants:

/* Matches elements inside [data-theme="corporate"] */
@custom-variant corporate (&:is([data-theme="corporate"] *));

/* Now you can use corporate:bg-blue-500 */

Automatic Content Detection

Remember the content array in v3?

v3
content: [
  './app/**/*.{ts,tsx}',
  './components/**/*.{ts,tsx}',
  './lib/**/*.{ts,tsx}',
]

Forget to add a path? Your classes don't work. Add a new directory? Update the config.

In v4, Tailwind automatically detects your source files. No configuration needed.

It scans your project intelligently, ignoring node_modules and build outputs. One less thing to maintain.

New PostCSS Plugin

The PostCSS setup changed from:

postcss.config.js (v3)
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
}

To:

postcss.config.js (v4)
export default {
  plugins: {
    "@tailwindcss/postcss": {},
  },
}

Note: autoprefixer is now built-in. You don't need it separately.

OKLCH Color Space

You'll notice v4 uses oklch() for colors instead of hex or hsl():

--primary: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);

OKLCH (Oklab Lightness Chroma Hue) is a perceptually uniform color space. That means:

  • Consistent perceived lightness - oklch(0.5 ...) looks equally bright regardless of hue
  • Better color manipulation - Adjusting lightness doesn't shift the hue
  • Wider gamut - Can represent colors outside sRGB on modern displays

You don't have to use OKLCH, but it's the new default for good reason.

What We Changed in Vibestacks

Here's how our globals.css looks in v4:

app/globals.css
@import "tailwindcss";
@import "tw-animate-css";

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

@theme inline {
  --color-background: var(--background);
  --color-foreground: var(--foreground);
  --color-primary: var(--primary);
  --color-primary-foreground: var(--primary-foreground);
  --color-secondary: var(--secondary);
  --color-muted: var(--muted);
  --color-accent: var(--accent);
  --color-destructive: var(--destructive);
  --color-border: var(--border);
  --color-ring: var(--ring);
  
  --font-sans: var(--font-geist-sans);
  --font-mono: var(--font-geist-mono);
  
  --radius-sm: calc(var(--radius) - 4px);
  --radius-md: calc(var(--radius) - 2px);
  --radius-lg: var(--radius);
  
  /* Animation tokens */
  --animate-marquee: marquee var(--duration) infinite linear;
  --animate-aurora: aurora 8s ease-in-out infinite alternate;
}

:root {
  --radius: 0.625rem;
  --background: oklch(1 0 0);
  --foreground: oklch(0.145 0 0);
  --primary: oklch(0.205 0 0);
  /* ... */
}

.dark {
  --background: oklch(0.145 0 0);
  --foreground: oklch(0.985 0 0);
  --primary: oklch(0.922 0 0);
  /* ... */
}

The pattern:

  1. @theme inline maps Tailwind tokens to CSS variables
  2. :root defines light mode values
  3. .dark defines dark mode values

This gives us full theming flexibility while keeping Tailwind's utility-first approach.

Migration Tips

Moving from v3 to v4? Here's the quick checklist:

1. Update Dependencies

pnpm remove tailwindcss postcss autoprefixer
pnpm add tailwindcss@latest @tailwindcss/postcss@latest

2. Update PostCSS Config

postcss.config.js
export default {
  plugins: {
    "@tailwindcss/postcss": {},
  },
}

3. Move Config to CSS

Take your tailwind.config.ts and convert it:

// v3 config
theme: {
  extend: {
    colors: {
      brand: '#3b82f6',
    },
  },
}

Becomes:

/* v4 CSS */
@theme inline {
  --color-brand: #3b82f6;
}

4. Delete tailwind.config.ts

You don't need it anymore. Delete it.

5. Update Dark Mode

Replace the darkMode: 'class' config with:

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

Skip the Migration

Migrating an existing project? It's tedious. You have to:

  1. Update all dependencies
  2. Rewrite your config in CSS
  3. Convert custom colors and animations
  4. Test everything for regressions
  5. Update any build tooling

Or you can start fresh.

Vibestacks ships with Tailwind v4 pre-configured. The theme system, dark mode, custom animations - it's all set up and ready to go. No migration required.

Check out Vibestacks →


Questions about Tailwind v4 or the migration? Reach out at support@vibestacks.io.