Getting CSS-first, @theme and Lightning CSS right
Tailwind CSS v4 is not an incremental update, it is a paradigm shift. The JavaScript configuration file gives way to pure CSS configuration, PostCSS is replaced by Lightning CSS, and the design token system now works directly through CSS custom properties. This guide shows how to structure the migration from v3 to v4 without introducing regressions.
Table of Contents
- 1. Why Tailwind CSS v4 is a paradigm shift
- 2. The automated upgrade tool
- 3. CSS-first configuration: from tailwind.config.js to @theme
- 4. Lightning CSS instead of PostCSS
- 5. Renamed and changed utility classes
- 6. New variants and selectors in v4
- 7. Migrating plugins: from JS to CSS
- 8. v3 vs. v4 head to head
- 9. Migration checklist for real projects
- 10. Summary
- 11. FAQ
1. Why Tailwind CSS v4 is a paradigm shift
The Tailwind CSS migration from v3 to v4 differs fundamentally from earlier minor updates. In Tailwind CSS v4 the entire architecture was rethought: instead of a JavaScript configuration file that the build tool calls into, the whole configuration now lives inside CSS itself. That means design tokens, breakpoints, colors and custom utilities are defined directly in CSS, without a single line of JavaScript. This approach is called CSS-first and is the defining feature of the new version.
The second major change concerns the internal processor. Tailwind CSS v4 relies on Lightning CSS, a CSS transformer written in Rust that is significantly faster than the previous PostCSS-based workflow. Full builds that took several seconds in v3 shrink down to milliseconds. This is a considerable advantage especially for large projects with many custom utilities and complex design token systems. The Tailwind CSS migration is therefore worthwhile not only for its new features, but for purely pragmatic performance reasons as well.
Still, the migration should not be underestimated. Anyone running a complex design system built on tailwind.config.js has to translate every configuration into the new CSS syntax. Plugins written in JavaScript also need to be rewritten. The automated upgrade tool helps enormously with this, but it does not cover every edge case. A structured Tailwind CSS migration with proper test coverage is therefore more important than ever.
2. The automated upgrade tool
Tailwind ships an official upgrade tool for the Tailwind CSS migration from v3 to v4 that automatically performs most of the mechanical renamings. The command itself is simple: npx @tailwindcss/upgrade inside the project directory. The tool analyzes every template file, detects deprecated classes and replaces them with the new equivalents. It also converts tailwind.config.js into a CSS @theme block wherever that is automatically possible.
The limits of the tool are real limits, though: complex plugin definitions written in JavaScript, dynamically computed class strings and theme-specific extend constructs still need to be reworked by hand. The upgrade tool produces a migration.log file that lists every change it made along with every spot that requires manual attention. This log file is the most important starting point for the manual phase of the Tailwind CSS migration. It is a good idea to run the tool on a separate branch and review the changes commit by commit.
/* Run the automated migration tool first */
/* npx @tailwindcss/upgrade */
/* BEFORE (v3): tailwind.config.js */
/*
module.exports = {
content: ['./src/**/*.{html,js}'],
theme: {
extend: {
colors: {
brand: { 500: '#0ea5e9', 700: '#0369a1' }
},
fontFamily: {
sans: ['Inter', 'sans-serif']
}
}
}
}
*/
/* AFTER (v4): main CSS file, no JS config needed */
@import "tailwindcss";
@theme {
/* Custom brand colors map directly to CSS custom properties */
--color-brand-500: #0ea5e9;
--color-brand-700: #0369a1;
/* Font family tokens */
--font-family-sans: Inter, sans-serif;
}
3. CSS-first configuration: from tailwind.config.js to @theme
The most important step in the Tailwind CSS migration is understanding the @theme directive. In Tailwind v4 every design token is defined as a CSS custom property under @theme. Tailwind automatically generates utility classes from it: from --color-brand-500: #0ea5e9 come the classes text-brand-500, bg-brand-500, border-brand-500 and every other color-related utility. The mapping from token name to class follows a consistent scheme derived directly from the variable name.
Custom utilities that were defined in v3 through addUtilities() inside plugins are now written directly in CSS in v4 using the @utility directive. That brings the definition of custom classes much closer to standard CSS and no longer requires any JavaScript knowledge. Existing @apply calls remain broadly compatible in v4, but should be critically reviewed during the Tailwind CSS migration, in some cases a direct CSS rule is clearer and faster. The @layer directive still works, but now interacts with the expanded cascade layer system.
Breakpoints and container queries are also configured through CSS variables in v4. The default breakpoint for md is defined as --breakpoint-md: 48rem and can be overridden through @theme. This consistency of the CSS-first approach is the real core of the new architecture: everything Tailwind needs to know about the project lives inside the CSS file itself, no external configuration file, no second context.
4. Lightning CSS instead of PostCSS
In Tailwind CSS v4, Lightning CSS takes over the tasks that PostCSS plugins used to handle: autoprefixing, CSS nesting, oklab() and oklch() color functions, logical properties and modern CSS syntax are all transformed directly by Lightning CSS. That means anyone carrying out the Tailwind CSS migration can remove postcss-nesting, autoprefixer and similar plugins from the dependency list. The postcss.config.js becomes noticeably shorter or disappears entirely in Vite projects.
The Tailwind CSS v4 Vite plugin @tailwindcss/vite integrates Lightning CSS transparently into the build process. For projects without Vite there is @tailwindcss/postcss, which acts as a PostCSS plugin but relies on Lightning CSS internally. During the Tailwind CSS migration, the choice of integration path needs to be made early on, since it affects the configuration of the entire build system. The speed gains are immediately noticeable in large projects: incremental rebuilds typically drop to under 100 milliseconds.
/* Vite integration, replaces postcss.config.js for most projects */
/* vite.config.ts */
/*
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()]
})
*/
/* PostCSS integration (non-Vite projects) */
/* postcss.config.js */
/*
export default {
plugins: {
'@tailwindcss/postcss': {}
// autoprefixer and postcss-nesting are NO LONGER needed
// Lightning CSS handles all transformations internally
}
}
*/
/* In your main CSS file, that's all you need */
@import "tailwindcss";
@theme {
--color-primary: oklch(60% 0.2 250);
--color-primary-dark: oklch(45% 0.2 250);
}
/* Lightning CSS transforms oklch() automatically for older browsers */
.btn-primary {
background-color: var(--color-primary);
&:hover {
background-color: var(--color-primary-dark);
}
}
5. Renamed and changed utility classes
Besides the configuration architecture, the Tailwind CSS migration involves a number of renamed classes. The most important changes affect shadow utilities: shadow became shadow-sm, shadow-sm became shadow-xs. The blur system was renamed analogously. The old class names are replaced by the automatic upgrade tool, but template files that assemble classes dynamically still need to be checked by hand.
Ring utilities have changed: ring now produces a 1px ring instead of 3px as in v3. Anyone who wants to keep the old visual look needs to switch to ring-3. Outline utilities were unified and replace some of the earlier ring patterns. Opacity utilities were integrated directly into the affected classes in v4: bg-blue-500/50 was already possible in v3, but in v4 this pattern is the only recommended way to apply color opacity. The bg-opacity-* classes are gone. During the Tailwind CSS migration, a global search for bg-opacity, text-opacity and border-opacity should be the first step.
6. New variants and selectors in v4
Tailwind CSS v4 introduces several new variants that were not available in v3. The field-sizing variant, starting for view transitions, inert for disabled elements and nth-* for CSS :nth-child() selectors are all natively included in v4. These variants no longer require plugins. Anyone who used custom plugins for such selectors in v3 can remove them as part of the Tailwind CSS migration.
Particularly interesting is the new built-in support for container queries, no plugin required. In v3, @tailwindcss/container-queries was an external package. In v4 it is built in: @container, @sm:text-lg and named containers work out of the box. This considerably simplifies component-based layouts, because breakpoints are now defined relative to the parent element instead of the viewport. During the Tailwind CSS migration, projects that used the container query plugin can remove it from their dependencies.
7. Migrating plugins: from JS to CSS
Anyone who wrote Tailwind plugins in JavaScript in v3 faces the most demanding task in the Tailwind CSS migration. The JavaScript plugin API from v3 (plugin(({ addUtilities, addComponents, theme }) => {})) still exists in v4 for backward compatibility, but it is no longer the recommended path. In v4, new utilities are defined through @utility, new component classes through @layer components, and design tokens through @theme.
A plugin that registered custom utilities via addUtilities in v3 becomes a plain block in the CSS file in v4. The benefit is considerable: the utilities are now standard CSS and can be read and understood by developers who do not know any JavaScript. They also benefit from Lightning CSS optimization. For complex plugins that read dynamically from the theme object (theme('colors.brand')), the migration is more involved, here the CSS custom properties generated by @theme need to be used directly.
/* BEFORE (v3): JavaScript plugin */
/*
const plugin = require('tailwindcss/plugin')
module.exports = {
plugins: [
plugin(({ addUtilities }) => {
addUtilities({
'.text-balance': { 'text-wrap': 'balance' },
'.text-pretty': { 'text-wrap': 'pretty' },
})
})
]
}
*/
/* AFTER (v4): Pure CSS, no plugin needed for most cases */
@import "tailwindcss";
@utility text-balance {
text-wrap: balance;
}
@utility text-pretty {
text-wrap: pretty;
}
/* Accessing theme tokens in utilities */
@utility btn-brand {
background-color: var(--color-brand-500);
color: white;
padding: var(--spacing-2) var(--spacing-4);
border-radius: var(--radius-lg);
}
8. v3 vs. v4 head to head
The table below shows the most important conceptual differences between Tailwind CSS v3 and v4 that need to be considered during the Tailwind CSS migration.
| Aspect | Tailwind v3 | Tailwind v4 | Migration effort |
|---|---|---|---|
| Configuration | tailwind.config.js |
@theme in CSS |
High, but tool-assisted |
| CSS processor | PostCSS | Lightning CSS | Adjust build configuration |
| Plugins | JavaScript API | @utility / @layer CSS | Medium, needs rewriting |
| Container queries | External plugin | Built in | Remove plugin |
| Shadow / Ring | Old scale | New naming | Low, upgrade tool |
9. Migration checklist for real projects
A successful Tailwind CSS migration from v3 to v4 follows a clear order. First, run the upgrade tool on a feature branch and review the log file. Then update the build configuration: set up the Vite plugin or the PostCSS plugin, and remove the PostCSS plugins that Lightning CSS now takes over. After that, manually move any leftover pieces of tailwind.config.js that the tool could not translate automatically into @theme blocks.
In the next step, search the entire codebase for the terms bg-opacity, text-opacity, border-opacity, standalone shadow and standalone ring, and replace them with the v4 equivalents. Then review every JavaScript plugin and decide which ones move into @utility blocks and which ones keep the JavaScript API for the time being for compatibility reasons. Finally, run visual regression tests, Chromatic, Percy or simple screenshot comparisons secure UI integrity after the Tailwind CSS migration.
10. Summary
The Tailwind CSS migration from v3 to v4 is more involved than a normal minor version bump, but the benefits justify the effort: significantly faster build times through Lightning CSS, a single source of configuration in CSS, built-in container queries and a more consistent design token system. The automated upgrade tool covers the bulk of the mechanical changes, but complex plugin landscapes and dynamic class strings still need manual attention.
Anyone who approaches the migration in a structured way, upgrade tool, build configuration, manual follow-up, visual tests, significantly reduces the risk of regressions. Tailwind CSS v4 is the most stable and fastest foundation Tailwind has ever offered, and the investment in the Tailwind CSS migration pays off for any project that stays in production for longer than a year.
Tailwind CSS Migration v3 to v4: the essentials at a glance
CSS-first configuration
tailwind.config.js is gone. Every design token is defined under @theme in the CSS file. Lightning CSS takes care of autoprefixing and CSS nesting.
Upgrade tool
npx @tailwindcss/upgrade automates renamings and produces a migration log. Manual follow-up is needed for plugins and dynamic classes.
Breaking changes
Shadow/ring scale changed, opacity utilities removed, plugin API moved into CSS directives. Container queries are now built in.
Build integration
@tailwindcss/vite for Vite projects, @tailwindcss/postcss for everything else. PostCSS plugins like autoprefixer can be removed.