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 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:
| Feature | Tailwind v3 | Tailwind v4 |
|---|---|---|
| Configuration | tailwind.config.ts (JavaScript) | @theme in CSS |
| Build tool | PostCSS plugin | New @tailwindcss/postcss |
| Performance | Fast | 40-60% faster |
| CSS variables | Optional | Native, first-class |
| Dark mode | darkMode: 'class' config | @custom-variant dark |
| Content detection | Manual content array | Automatic |
Let's break down each change.
No More JavaScript Config
In Tailwind v3, you'd configure everything in tailwind.config.ts:
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 configIn v4, all of this moves to CSS:
@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?
- No JavaScript in your styling - CSS stays in CSS
- IDE support - CSS syntax highlighting and autocomplete work everywhere
- Faster builds - No JavaScript parsing or execution
- 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-primaryyou 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:
| Prefix | Generates | Example |
|---|---|---|
--color-* | Color utilities | bg-brand, text-brand |
--font-* | Font family utilities | font-sans, font-mono |
--animate-* | Animation utilities | animate-fade-in |
--radius-* | Border radius utilities | rounded-lg |
--spacing-* | Spacing utilities | Custom 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:
module.exports = {
darkMode: 'class',
// ...
}In v4, it's a CSS directive:
@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?
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:
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}To:
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:
@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:
@theme inlinemaps Tailwind tokens to CSS variables:rootdefines light mode values.darkdefines 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@latest2. Update PostCSS Config
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:
- Update all dependencies
- Rewrite your config in CSS
- Convert custom colors and animations
- Test everything for regressions
- 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.
Questions about Tailwind v4 or the migration? Reach out at support@vibestacks.io.
Read more

Why We Use cn() and cva() for Component Styling
String concatenation for Tailwind classes is a mess. Here's how cn() and cva() make conditional styling clean, type-safe, and maintainable.

Stop Trusting process.env: Type-Safe Env Variables in Next.js
process.env fails silently and leaks secrets. Here's how t3-env catches missing env vars at build time, not production.

Beyond Pageviews: Why We implemented Analytics into vibestacks PRO
Shipping is only step one. Here's why we pre-configured PostHog to track user intent, not just traffic and why it matters for your growth.