A practical walkthrough of what actually breaks when you upgrade to Tailwind CSS v4, from the silent border-color default to the renamed shadow scale.

Tailwind v4 shipped a new Rust-powered engine, killed the JavaScript config file, and quietly redefined what border, shadow-sm, and ring mean in your components. The upgrade codemod handles most of the mechanical rewrites, but a clean diff doesn't guarantee a clean UI. This guide walks through what actually changes when you run the upgrade, where the silent regressions hide, and how to migrate a real codebase without shipping broken visuals.
If you're maintaining anything from a marketing site to a dashboard with thousands of components, v4 is not a drop-in upgrade. The new engine (Lightning CSS, written in Rust) delivers roughly 3.5x faster full builds and around 8x faster incremental builds, which is a real developer-experience win on large projects. But two defaults quietly shifted: border-* utilities no longer inherit your configured gray, and the entire shadow scale was renamed. Either change alone can cause subtle visual regressions across a product. Combine them with the new CSS-first configuration and the removal of the tailwind.config.js file, and you have a migration that looks small on paper but touches every layer of your styling setup.
The good news: the v4 architecture is closer to how CSS actually works in 2026. Native cascade layers, container queries, CSS variables you can read from any component, zero-config content detection, all first class. Once you're past the migration, the mental model is cleaner.
There are three architectural shifts worth understanding before you touch any code.
The engine. v3 compiled via PostCSS and a JavaScript-centric pipeline. v4 uses a Rust-based engine derived from Lightning CSS. You stop installing tailwindcss as a PostCSS plugin and instead use @tailwindcss/postcss (or @tailwindcss/vite if you're on Vite). That rename alone breaks any custom build step that imports the old plugin path.
The config. The tailwind.config.js file is no longer the source of truth. Configuration moves into CSS using the @theme directive. This isn't just cosmetic. Your design tokens become real CSS custom properties, reachable from any component via var(--color-primary). The file @import "tailwindcss" replaces the old trio of @tailwind base; @tailwind components; @tailwind utilities;.
The defaults. This is where the silent regressions live. In v3, border meant 1px solid using your configured gray-200. In v4, it uses currentColor, matching native CSS behavior. Any component that relied on the implicit gray border will now inherit text color. The shadow scale was also renamed: what used to be shadow-sm is now shadow-xs, and what used to be shadow is now shadow-sm. If you never update the class names, your existing shadow-sm elements will suddenly look heavier because they're now rendering the old shadow.
A few other renames to know about: bg-gradient-to-r is now bg-linear-to-r, flex-shrink-0 is now shrink-0, and the default ring width dropped from 3px to 1px.

Here's the sequence I'd follow on a non-trivial project.
Step 1: Check browser support. v4 targets Safari 16.4+, Chrome 111+, and Firefox 128+. It uses modern CSS features that don't polyfill cleanly. If your analytics show meaningful traffic from older browsers, stay on v3.4 until that changes.
Step 2: Run the official codemod. From the project root:
npx @tailwindcss/upgrade
This scans your HTML, JSX, TSX, Vue, Astro, and CSS files. It renames aliased utilities, migrates your tailwind.config.js into a CSS @theme block, updates your PostCSS config, and bumps dependencies. It prints a diff of every file it touched. Commit that diff on its own branch.
Step 3: Read the diff. The codemod handles roughly 90% of the mechanical work, but it cannot see your intent. Look for template literal class names, dynamic class construction, and any custom plugins. These often escape the codemod.
Step 4: Do a UI pass. This is the step most migration tutorials skip. Spin up Storybook, your design system, or your staging environment and compare against production. Pay special attention to anything using border without an explicit color, any shadow-sm elements, and focus states using ring.
Step 5: Update your custom config. If you had a large tailwind.config.js, the codemod may have left some pieces behind (plugins, presets, complex functions). Move those to CSS or to a v4-compatible plugin API.
Here's what the same design tokens look like in v3 versus v4.
v3 (tailwind.config.js):
module.exports = {
content: ["./src/**/*.{html,js,jsx,ts,tsx}"],
theme: {
extend: {
colors: {
brand: "#2563eb",
surface: "#f8fafc",
},
fontFamily: {
display: ["Inter", "sans-serif"],
},
},
},
};
v4 (app.css):
@import "tailwindcss";
@theme {
--color-brand: #2563eb;
--color-surface: #f8fafc;
--font-display: "Inter", sans-serif;
}
Two things to notice. First, the content array is gone. v4 auto-detects template files in your project and excludes .gitignore paths. Second, each token becomes a real CSS variable, so you can write color: var(--color-brand) in arbitrary CSS without needing theme() helpers.

Trusting the codemod on dynamic classes. If you build class names with string concatenation (`shadow-${size}`), the codemod won't rename them. Grep your codebase for constructed class names after the upgrade.
Skipping the visual diff. The border default change is the single biggest source of regressions. Components that relied on implicit gray-200 borders will render with text color, which can look surprisingly broken on colored text or dark backgrounds. Set an explicit default in your base layer if you want the old behavior:
@layer base {
*, ::before, ::after {
border-color: var(--color-gray-200, currentColor);
}
}
Keeping the old PostCSS plugin. tailwindcss as a PostCSS plugin is gone. Replacing it with @tailwindcss/postcss is a two-line change but it's not something the codemod will always fix if your config lives in a non-standard location.
Ignoring custom plugins. v3 plugins written against the old JS API won't work unchanged. Some plugins have v4 equivalents; others you'll need to rewrite as CSS using @utility and @variant.
Assuming dark: still works the same way. Dark mode configuration moved. The default is class-based via a @variant declaration in your CSS rather than the darkMode key in JS.
Treat @theme as your single source of design tokens. If a value belongs in the design system, put it in @theme so it becomes a utility and a CSS variable at once. Use :root only for runtime CSS variables that shouldn't generate utilities, like computed layout values.
Lean on container queries. v4 ships them as first-class utilities (@container, @sm, @md), and they're the right tool for component-level responsive design. Breakpoints based on the viewport are increasingly the wrong primitive for a component library.
Pin your version. v4 is still iterating fast, with meaningful additions landing in patch releases (text shadows and mask utilities arrived in 4.1). Pin the exact version and read the release notes before bumping.
Keep a small compat.css file during migration. If you need to preserve a v3 behavior for a subset of components, a scoped override is cleaner than littering your templates with explicit color and shadow classes.
Tailwind v4 is a real architectural reset, not a version bump. The performance wins are genuine, the CSS-first config is better aligned with how modern CSS works, and the engine is fast enough that incremental builds feel instant. But the migration deserves the same care as any breaking upgrade. Run the codemod, read the diff, do a visual pass, and budget time for the custom plugin and dynamic class edge cases. If you've been putting this off because "the codemod handles it," that's the part that usually isn't true. Handle the last 10% on purpose and v4 pays off quickly.
Comments
Sign in to join the discussion.
No comments yet. Be the first to share your thoughts.