Back to Blog · Software Architecture

Tailwind v4 CSS-First Config: Deleting tailwind.config.js for a theme Block

How Tailwind CSS v4 replaces the old JavaScript config file with a CSS-native @theme block, and what that means for theme management, build tooling, and the developer experience gap between v3 and v4.

MF
Martin Fournier
· June 13, 2026 · 5 MIN READ
Illustration for: Tailwind v4 CSS-First Config: Deleting tailwind.config.js for a theme Block

Tailwind CSS v4 ships a radical departure from every prior version: it drops tailwind.config.js entirely in favor of CSS-first configuration. Instead of theme.extend and content paths in JavaScript, you use @import "tailwindcss" and a @theme block in your app.css. This is not a cosmetic rename. It changes how you think about design tokens, utility generation, and build pipelines.

This site (martinfournier.com) went through that migration. Here is what changed, what broke, and what got better.

The old contract

In Tailwind v3, the config file owned everything: color palettes, font stacks, breakpoints, animations, content globs, and plugin registration. Every project started with the same ritual: create tailwind.config.js, pull in @tailwindcss/typography via plugins, extend the default theme with custom colors, and point content at your Blade templates. If you wanted a custom animation, you wrote a keyframe in CSS and registered an animation key in the config. The design tokens lived in two places: the config for Tailwind to compile, and the CSS for the browser to run.

// tailwind.config.js (v3)
module.exports = {
  content: [
    "./resources/**/*.blade.php",
    "./vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php",
  ],
  theme: {
    extend: {
      colors: {
        surface: { 900: "#0a0a0a", 800: "#111111", 700: "#1a1a1a" },
        amber: { 600: "#d97706" },
      },
      fontFamily: {
        sans: ["Inter", "ui-sans-serif", "system-ui", "sans-serif"],
      },
    },
  },
  plugins: [require("@tailwindcss/typography")],
};

The new contract

Tailwind v4 moves everything into CSS. The config file disappears. In its place, you write the same information directly in your stylesheet:

@import "tailwindcss";
@plugin "@tailwindcss/typography";

@source "../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php";
@source "../**/*.blade.php";

@theme {
  --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
  --color-surface-900: #0a0a0a;
  --color-surface-800: #111111;
  --color-amber-600: #d97706;
  --animate-fade-up: fade-up 0.65s cubic-bezier(0.22, 1, 0.36, 1) both;
}

@keyframes fade-up {
  from { opacity: 0; transform: translateY(18px); }
  to   { opacity: 1; transform: translateY(0); }
}

Three directives replace the old JavaScript:

  • @plugin registers npm packages like @tailwindcss/typography. No module.exports wrapping, no require() call.
  • @source tells the CLI where to scan for class usage. Instead of fragile glob strings in a JS array, you declare content roots directly in CSS.
  • @theme defines design tokens as CSS custom properties. Every --font-, --color-, --animate-* key becomes a utility class. The naming convention is hardlinked: --color-amber-600 generates bg-amber-600, text-amber-600, border-amber-600.

What improved

First, single source of truth. Design tokens now live where designers and browsers already look: the stylesheet. You never bounce between config.js and app.css to understand what --color-amber-600 resolves to.

Second, content detection is smarter. The @source directive can accept file globs or directory paths. The v4 CLI has built-in content detection that reads your templates without you telling it what to scan. For Laravel projects, one @source statement covers all Blade files. You no longer forget to add a new view directory to the content array.

Third, animations stay colocated. In v3, the keyframes lived in CSS and the animation shorthand lived in the config. In v4, both live in CSS: @keyframes in the stylesheet, --animate-fade-up in @theme. When you delete an animation, you delete one block in one file instead of hunting through two locations.

What broke

Migrating from v3 to v4 is not seamless. The @tailwindcss/vite plugin replaces the PostCSS plugin entirely. You need to install @tailwindcss/vite and register it in your vite.config.js:

import tailwindcss from "@tailwindcss/vite";

export default defineConfig({
  plugins: [laravel(), tailwindcss()],
});

If your project used @apply with complex class combinations, expect churn. Tailwind v4 rewrote the @apply implementation to be stricter about circular references and cascade order. Some v3 @apply rules compile silently in v3 and error in v4.

Variant ordering changed. The arbitrary variant syntax (e.g., max-lg:) was overhauled. RTL/LTR variants now use the CSS logical properties spec instead of Tailwind's old directional prefixes. If you had custom variants in your config, they need porting to the new CSS-based variant system.

Custom containers and screens defined via theme.screens in v3 now use --breakpoint-* tokens in @theme. The syntax is simpler, but any v3 theme.extend that used complex breakpoint overrides requires manual rework.

The migration path

For a Laravel project like this one, the migration follows a predictable sequence:

  1. Upgrade the npm packages: tailwindcss ^3 -> ^4, add @tailwindcss/vite.
  2. Replace vite.config.js PostCSS plugin with the new Vite plugin.
  3. Delete tailwind.config.js. Move theme.extend to @theme in app.css.
  4. Convert plugins: require("@tailwindcss/typography") becomes @plugin "@tailwindcss/typography".
  5. Replace @tailwind base/components/utilities with a single @import "tailwindcss".
  6. Test every page. Fix @apply churn, variant changes, and any utility classes that shifted.

The full migration takes about an hour for a moderately themed site. Most of the time goes to @apply breakage and variant edge cases, not the token transfer itself.

Is it worth it?

Yes. The CSS-first config removes the configuration duality that always bothered me about Tailwind. Every other frontend framework (Vanilla Extract, Panda CSS, StyleX) has moved toward code-first or CSS-first design token management. Tailwind was the last holdout with a proprietary JavaScript DSL for what is fundamentally a CSS problem.

The @theme block is not just a reimplementation. The CSS custom property approach means you can inspect design tokens in the browser devtools, override them at runtime via inheritance, and compose them with native CSS functions like calc() and color-mix(). None of that was possible with the old JavaScript config.

For new projects, start with v4. For existing v3 projects, the migration is finite and the output is cleaner. Delete the config file. Move the tokens into CSS. One less abstraction layer between you and what the browser sees.