Tailwind v4 Migration - CSS-First Config, @theme, and Common Breakages
Status: Active
Last Updated: 2026-08-26
Category: Frontend - Styling
Prerequisites: website-rebuild, component-library-notes
Time: 2 hours
Tags: tailwindcss, tailwind-v4, css-first-config, migration, postcss
Summary
What changed between Tailwind CSS v3 and v4, how the fogserv.cloud website straddles the line today (v4 PostCSS plugin installed, but a legacy tailwind.config.ts still present), a step-by-step migration to pure CSS-first configuration with @theme, and the breakages you'll hit along the way.
๐ฏ What You'll Learn
By the end of this article, you'll be able to:
- โ
Explain v3โv4 differences:
@import "tailwindcss",@theme, automatic content detection - โ
Migrate theme tokens from
tailwind.config.tsinto CSS@theme - โ
Update plugins (
typography) and drop obsolete PostCSS entries - โ Diagnose the classic v4 breakage list (colors vanish, plugin styles missing, dark mode flips)
Context / Why This Matters
The website (website-rebuild) was built on Tailwind v4 โ @tailwindcss/postcss is in postcss.config.js โ but the repo still carries a v3-era tailwind.config.ts mapping brand colors (primary, secondary, accent tan/terracotta) to CSS variables. That hybrid works only because v4 auto-detects config files; it's deprecated behavior, and it confuses contributors about where tokens live. Finishing the migration makes the design system single-sourced in CSS.
Implementation / Core Content
What Changed in v4
| v3 | v4 |
|---|---|
tailwind.config.js/ts required for content + theme |
Optional; content detection automatic, theme lives in CSS |
@tailwind base; @tailwind components; @tailwind utilities; |
Single @import "tailwindcss"; |
| Theme extensions in JS objects | @theme { --color-primary: ... } in CSS |
| Plugins loaded via config array | @plugin "@tailwindcss/typography"; in CSS |
PostCSS: tailwindcss plugin |
PostCSS: @tailwindcss/postcss package |
dark: = media strategy by default |
Same, but @custom-variant dark to opt into class strategy |
| Opacity modifier math | Uses native color-mix() |
Current State Audit (this repo)
// postcss.config.js โ correct for v4
export default {
plugins: {
'@tailwindcss/postcss': {},
},
}
// tailwind.config.ts โ legacy v3-style, to be retired after token migration
export default {
content: ['./src/**/*.{ts,tsx}'],
theme: { extend: { colors: {
primary: { DEFAULT: 'var(--color-primary)', hover: 'var(--color-primary-hover)', ... },
...
Note the indirection: config points at CSS variables. In v4's @theme, those variables are the theme, so one layer disappears.
Step-by-Step Migration
Replace directives in
src/index.css:/* before */ @tailwind base; @tailwind components; @tailwind utilities; /* after */ @import 'tailwindcss';Move tokens into
@theme, translating the config's variable references into direct definitions (keep the same variable names so existing usage keeps working):@theme { --color-primary: #2f6f5e; /* actual brand value */ --color-primary-hover: color-mix(in oklab, var(--color-primary) 85%, black); --color-primary-light: color-mix(in oklab, var(--color-primary) 80%, white); --color-secondary: ...; --color-accent-tan: ...; --color-accent-terracotta: ...; --font-display: ..., sans-serif; }Utilities like
bg-primary,text-accent-tan-hoverare generated from these names automatically.Load plugins in CSS, not config:
@plugin '@tailwindcss/typography';(
@tailwindcss/typographyis already a dependency.)Delete
tailwind.config.tsonce nothing references it, then remove any leftoverautoprefixerentry โ v4 handles vendor prefixing internally.Verify visual parity: build and diff-render key pages (
/,/kb/,/admin) against screenshots before merging.
If any v3-only syntax survives (e.g., class-strategy dark mode), declare it explicitly:
@custom-variant dark (&:where(.dark, .dark *));
Why Migrate at All?
- One source of truth for the design system (CSS), readable without TS context.
- No config auto-detection magic โ explicit imports behave identically everywhere.
- Unlocks v4-native features:
@variant, container queries as first-class utilities, modern color functions.
Practical Examples
Example 1: Before/after for one brand token
// v3 (tailwind.config.ts)
primary: { DEFAULT: 'var(--color-primary)', hover: 'var(--color-primary-hover)' }
/* v4 (@theme in index.css) */
@theme {
--color-primary: var(--brand-primary); /* keep runtime theming if desired */
--color-primary-hover: var(--brand-primary-hover);
}
Class names (bg-primary, hover:bg-primary-hover) are unchanged โ components need no edits.
Example 2: Verifying migration completeness
grep -rn "tailwind.config" src/ vite.config.ts app.config.ts # expect no hits
bun run build && bun run preview # visual smoke test
grep -c "@theme" src/index.css # โฅ 1
Example 3: Typography plugin check
Any page rendering markdown via react-markdown should use prose prose-primary; after migrating the plugin load, confirm headings/code blocks still style correctly โ a missing @plugin line shows as unstyled article HTML.
Common Pitfalls & Troubleshooting
| Problem | Cause | Fix |
|---|---|---|
| All custom colors render as plain CSS vars / transparent | Tokens still only in legacy config; import order wrong | Move tokens into @theme; ensure @import "tailwindcss" precedes @theme |
prose classes do nothing |
Typography plugin not loaded under v4 | Add @plugin '@tailwindcss/typography'; to index.css |
npx tailwindcss init errors or warns |
v4 has no CLI-init workflow | Don't re-create a config; use @theme |
Dark mode stops responding to .dark class |
v4 default is media strategy | Declare @custom-variant dark (&:where(.dark, .dark *)); |
| Build fails: "Cannot apply unknown utility class" | @apply referencing classes defined only in old config |
Define token in @theme first, or replace @apply with plain CSS vars |
| Content not scanned (styles missing on some pages) | Old mental model: manual content globs needed |
v4 scans automatically; check the file isn't gitignored instead |
Duplicate autoprefixer warnings |
Leftover postcss plugin | Remove autoprefixer from postcss config and devDependencies |
Next Steps / Ops Actions
- Schedule the token migration PR; screenshot-compare
/,/kb/,/changelogbefore merge. - After deletion, document tokens only in component-library-notes.
- Keep CI green on
bun run type-checkand build so regressions can't land (woodpecker-first-pipeline).
Sources & Related
External references consulted:
- https://tailwindcss.com/docs/upgrade-guide
- https://tailwindcss.com/docs/theme
- https://github.com/tailwindlabs/tailwindcss-typography
Related knowledge-base articles:
Change Log
2026-08-26
- Initial creation covering v3โv4 changes, migration steps, and breakage troubleshooting.