Back to Blog

Moving to Tailwind v4: No More tailwind.config.js

#tailwindcss#css#frontend
Moving to Tailwind v4: No More tailwind.config.js

Tailwind v4 completely changed how configuration works — there’s no tailwind.config.js anymore, everything is defined directly in CSS with the @theme directive. This site has used that approach from the start, though I was skeptical the first time I read the changelog, since it sounded like a breaking change that would be a hassle for an already-running project.

src/styles/global.css has a single @theme block defining --color-primary: #137fec, --color-primary-dark, --color-background-light/dark, --color-surface-light/dark, and --font-sans for the Inter font stack. Every one of those tokens automatically becomes a Tailwind utility class — bg-primary, text-primary-dark, and so on — without a single line of JavaScript. No theme.extend.colors anymore, no separate config file that has to stay in sync with the CSS.

What makes this nicer for me: back in v3, adding a new color meant opening a separate JS file, editing a theme.extend.colors object, and sometimes restarting the dev server for the change to take effect — a step that was easy to forget while heads-down in CSS. Now I edit it in the same file where I write everything else, and Vite hot-reloads the change instantly without any restart. Sounds small, but the number of context-switches that disappear adds up noticeably during fast iteration on colors and spacing.

One more detail I like: dark mode on this site doesn’t use Tailwind’s built-in dark: class strategy, which by default follows raw prefers-color-scheme — it uses a custom variant, @custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));, that follows a data-theme attribute on the <html> element instead. So the manual three-way theme toggle I built (system/light/dark, covered in another post) stays compatible with ordinary Tailwind utility classes like dark:bg-surface-dark, with no odd workarounds like duplicated classes or extra JavaScript to override styles.

The migration itself wasn’t fully painless. One older plugin I used in v3 for a custom animation wasn’t compatible with the new @theme system yet, so I had to rewrite a few keyframes as plain CSS instead of going through plugin config — an extra hour or two of work I hadn’t planned on when I decided to upgrade. For a bigger project than this personal site, with more custom plugins, I’d recommend reading the official migration changelog carefully before just running npm install tailwindcss@latest.

CSS-first config sounds like a small change on paper, but for a one-person project like this, one fewer configuration file means one fewer place to forget to update when something changes — and that’s the kind of friction reduction that shows up months later, not on the first day of migrating.