Tailwind v4: What Actually Changed
Tailwind v4: What Actually Changed
Tailwind 4 is a ground-up rewrite. Not a minor version bump with some new utilities — a fundamentally different architecture. ShockStack is built on v4 from the start, so here’s what’s different and why it matters.
Goodbye PostCSS, Hello Vite Plugin
The biggest change: Tailwind 4 doesn’t use PostCSS. It ships as a native Vite plugin.
Tailwind 3 setup:
// postcss.config.js
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
// tailwind.config.js
export default {
content: ["./src/**/*.{astro,vue,ts,tsx}"],
theme: { extend: { /* ... */ } },
plugins: [],
};
Tailwind 4 setup:
// vite.config.ts (or astro.config.ts)
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
plugins: [tailwindcss()],
});
That’s it. No postcss.config.js. No tailwind.config.js. No content array — Tailwind 4 automatically detects your source files. Two config files reduced to zero.
In ShockStack’s Astro config, it looks like this:
import { defineConfig } from "astro/config";
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
vite: {
plugins: [tailwindcss()],
},
});
CSS-First Configuration
In Tailwind 3, you customized the theme in JavaScript:
// tailwind.config.js (v3)
export default {
theme: {
extend: {
colors: {
primary: "#bd93f9",
secondary: "#44475a",
},
fontFamily: {
sans: ["Inter", "system-ui", "sans-serif"],
},
borderRadius: {
lg: "0.5rem",
},
},
},
};
In Tailwind 4, you do it in CSS with the @theme block:
/* global.css (v4) */
@import "tailwindcss";
@theme {
--color-primary: #bd93f9;
--color-secondary: #44475a;
--font-sans: Inter, system-ui, sans-serif;
--radius-lg: 0.5rem;
}
This is a big shift. Your theme is now CSS, living alongside your styles. No more context-switching between a JS config file and your stylesheets.
The @theme Block
The @theme block is how you tell Tailwind “these are the values I want to use in my utility classes.” Whatever you define in @theme becomes available as utilities.
@theme {
--color-brand: #6366f1;
}
Now bg-brand, text-brand, border-brand all work. The naming convention follows the CSS variable name: --color-{name} creates color utilities, --font-{name} creates font utilities, --radius-{name} creates border-radius utilities.
This is how ShockStack connects design tokens to Tailwind. The token pipeline generates CSS variables with --ss- prefixes, and the @theme block maps them to Tailwind’s namespace:
@import "../../../packages/tokens/dist/tokens.css";
@import "tailwindcss";
@theme {
--font-sans: var(--ss-font-family-sans);
--font-mono: var(--ss-font-family-mono);
--color-bg-primary: var(--ss-color-bg-primary);
--color-bg-secondary: var(--ss-color-bg-secondary);
--color-bg-tertiary: var(--ss-color-bg-tertiary);
--color-fg-primary: var(--ss-color-fg-primary);
--color-fg-secondary: var(--ss-color-fg-secondary);
--color-accent-purple: var(--ss-color-accent-purple);
--color-accent-pink: var(--ss-color-accent-pink);
--color-accent-green: var(--ss-color-accent-green);
--color-accent-cyan: var(--ss-color-accent-cyan);
--color-border-default: var(--ss-color-border-default);
--radius-sm: var(--ss-radius-sm);
--radius-lg: var(--ss-radius-lg);
--shadow-md: var(--ss-shadow-md);
}
Because the --ss-* variables change when data-theme switches (default tokens on :root, theme overrides on [data-theme="..."]), every Tailwind utility automatically adapts to the active palette. No dark mode variant needed.
What Happened to darkMode?
In Tailwind 3, you’d configure dark mode in the config:
// v3
export default {
darkMode: "class", // or "media"
// ...
};
Then use dark: variants everywhere:
<div class="bg-white dark:bg-gray-900 text-black dark:text-white">
ShockStack doesn’t need this pattern at all. Since design tokens change via CSS variables when the theme switches, a single class handles all built-in palettes:
<div class="bg-bg-primary text-fg-primary">
No dark: prefix. The CSS variables resolve to the right values based on the active theme. The same markup works across every built-in palette.
No More content Array
Tailwind 3 required you to tell it where your templates live:
// v3
export default {
content: [
"./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}",
],
};
Forget to add a path? Your classes get purged and nothing renders. It was a constant source of “why isn’t my class working” debugging.
Tailwind 4 uses automatic content detection. It scans your project based on imports and module graph — no config needed. If your file is part of the build, Tailwind sees it.
New Default Scale
Tailwind 4 ships a refined default color palette and spacing scale. The naming is also cleaner — text-gray-900 is still there, but the scale is more consistent.
For ShockStack, this doesn’t matter much since we override all color and spacing values with design tokens. But if you use any default Tailwind values alongside the token system, you’ll find them slightly different from v3.
Migration Checklist
If you’re coming from a Tailwind 3 project:
- Remove
postcss.config.jsandtailwind.config.js - Install
@tailwindcss/viteinstead oftailwindcssandautoprefixer - Add the Vite plugin to your build config
- Replace
@tailwind base/components/utilitieswith@import "tailwindcss" - Move theme customizations from
tailwind.config.jsto a@themeblock - Remove the
contentarray (automatic detection handles it) - Test — some edge cases around custom plugins may need updates
The migration is worth it. Less config, faster builds, and a more natural CSS-first workflow.