Mike Codeur Formations
Sign in
Mike Codeur Formations

By Mike Codeur

© 2026 Mike Codeur Formations. All rights reserved.

Product

  • Courses
  • Documentation
  • Blog

Legal & Contact

  • Legal Notice
  • Privacy Policy
  • Contact
Get Started

Ready to get started ?

Join thousands of users building amazing things with our platform.

Get Started Free

No credit card required

Newsletter

Stay updated with our latest articles and web development news.

No spam, unsubscribe anytime

Back to articlesDesign

Tailwind CSS v4: configuration moves into your CSS

No more tailwind.config.js. v4 moves the whole configuration into your stylesheet, with @theme, @plugin and @custom-variant.

#tailwindcss#css#frontend#design
Mike Codeur
August 02, 20264 min
Design
…
Written by
MC
Mike Codeur
Author
Published on August 02, 2026

Related Articles

Introduction to Next.js 16Development

Introduction to Next.js 16

What actually changes when you build with Next.js 16 and React 19: async params, Server Components, Server Actions and Turbopack.

Aug 03, 20265 min
Read more
Tutorial
Tutorial

The Proxy Component in React

A simple pattern to shield your codebase from change: centralise an implementation instead of scattering it across every file.

Aug 01, 20265 min
Read more
The most visible change in Tailwind CSS v4 is not a new utility class: it is the disappearance of tailwind.config.js. Configuration now lives inside your CSS, and that changes how you wire a design system into a project.

The new entry point

Where v3 required three directives plus a JavaScript config file, v4 fits on one line:
@import 'tailwindcss';
@tailwind base;
@tailwind components;
@tailwind utilities;
Plus a tailwind.config.js to maintain alongside it.
A typical v4 project has no tailwind.config.js at the root at all. If you still have one after migrating, it usually means something is left to move into the CSS.

@theme: your tokens become utilities

This is the heart of v4. What you declare in @theme does double duty: the CSS variables are exposed at runtime, and Tailwind generates the matching classes.
@import 'tailwindcss';

@theme {
  --color-background: var(--background);
  --color-foreground: var(--foreground);
}
Declaring --color-background is enough to get bg-background, text-background and border-background. The token name drives the available utilities: the --color-* prefix produces colour classes, --spacing-* spacing, --font-* font families.
Why it matters on a real project: your design tokens are no longer copied from a JS file into CSS. There is one declaration, and the classes follow automatically.

Dark mode driven by a variant

In v4, class-based dark mode is declared with @custom-variant:
@custom-variant dark (&:is(.dark *));
That line says the dark: prefix applies as soon as an ancestor carries the .dark class. Exactly what you need when the theme is driven by JavaScript (a theme switcher, a stored preference) rather than by the system setting alone. Combined with @theme, you get a clean system: components never hard-code a colour, only tokens.
:root {
  --background: oklch(1 0 0);
  --foreground: oklch(0.15 0 0);
}

.dark {
  --background: oklch(0.15 0 0);
  --foreground: oklch(0.98 0 0);
}
<div class="bg-background text-foreground">
  This block follows the theme without a single dark: class
</div>
The classic trap: writing bg-white dark:bg-slate-900 everywhere. It works, but every new colour becomes a double decision to maintain. Go through a token and dark mode comes for free.

Plugins move into CSS too

No more registering them in a config file:
@plugin 'tailwindcss-animate';
@plugin '@tailwindcss/typography';

OKLCH by default

v4 leans on oklch rather than hex. The benefit is concrete: lightness is perceptually uniform, so a palette generated by varying the first value stays visually consistent — something HSL never quite manages.
@theme {
  --color-primary: oklch(0.62 0.19 260);
}
Hex still works. There is no need to convert an entire existing palette on migration day: do it when you next revisit your colours.

Container queries built in

They no longer need a plugin: a component can react to the width of its container rather than the viewport.
<div class="@container">
  <div class="@sm:text-lg @md:flex @md:gap-6">
    Responsive to available space, not to the window
  </div>
</div>
This is the right answer for a component that must look good both full-width and inside a narrow sidebar — a case viewport breakpoints have never handled properly.

Migrating from v3

1

Replace the directives

The three @tailwind lines become a single @import 'tailwindcss'.
2

Move the theme

Whatever lived in theme.extend in the JS file goes into @theme as CSS variables.
3

Declare the plugins

Each plugin becomes one @plugin line in the CSS.
4

Redeclare dark mode

The darkMode: 'class' option becomes a @custom-variant line.
5

Delete the config file

Once everything has moved, tailwind.config.js has no reason to exist.
What lived in v3Where it goes in v4
theme.extend.colors@theme { --color-* }
theme.extend.spacing@theme { --spacing-* }
plugins: [...]@plugin '...'
darkMode: 'class'@custom-variant dark (...)
content: [...]Automatic detection
Do not migrate halfway. A config split between JS and CSS produces missing utilities with no clear error message — the kind of problem that costs an hour of debugging for one forgotten line.

Takeaway

v4 does more than relocate a file: it removes the boundary between "configuring Tailwind" and "writing CSS". A design system is declared once, in a stylesheet, and feeds both the CSS variables and the utility classes. For a project meant to last, that is one less source of truth to keep in sync.