Tailwind CSS v4: Wat Er Echt Veranderde en of Je Moet Migreren
CSS-first configuratie, @layer integratie, ingebouwde container queries, de nieuwe engine-prestaties, breaking changes en mijn eerlijke migratie-ervaring van v3 naar v4.
Ik gebruik Tailwind CSS al sinds v1.x, toen de helft van de community het een gruwel vond en de andere helft er niet mee kon stoppen met shippen. Elke major versie was een significante sprong, maar v4 is anders. Het is niet gewoon een feature release. Het is een architecturele herschrijving van de grond af die het fundamentele contract tussen jou en het framework verandert.
Na het migreren van twee productieprojecten van v3 naar v4 en het starten van drie nieuwe projecten op v4 vanaf nul, heb ik een helder beeld van wat oprecht beter is, wat nog ruw is, en of je vandaag moet migreren. Geen hype, geen verontwaardiging — gewoon wat ik heb waargenomen.
Het Grote Plaatje: Wat v4 Eigenlijk Is#
Tailwind CSS v4 is drie dingen tegelijk:
- Een nieuwe engine — herschreven van JavaScript naar Rust (de Oxide-engine), waardoor builds dramatisch sneller zijn
- Een nieuw configuratieparadigma — CSS-first configuratie vervangt
tailwind.config.jsals standaard - Een strakkere integratie met het CSS-platform — native
@layer, container queries,@starting-styleen cascade layers zijn eersteklas burgers
De kop die je overal zult zien is "10x sneller." Dat klopt, maar het onderschat de daadwerkelijke verandering. Het mentale model voor het configureren en uitbreiden van Tailwind is fundamenteel verschoven. Je werkt nu met CSS, niet met een JavaScript-configuratie-object dat CSS genereert.
Hier is hoe een minimale Tailwind v4-setup eruitziet:
/* app.css — dit is de hele setup */
@import "tailwindcss";Dat is het. Geen configuratiebestand. Geen PostCSS-pluginconfiguratie (voor de meeste setups). Geen @tailwind base; @tailwind components; @tailwind utilities; directives. Eén import, en je draait.
Vergelijk dat met v3:
/* v3 — app.css */
@tailwind base;
@tailwind components;
@tailwind utilities;// v3 — tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./src/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {},
},
plugins: [],
};// v3 — postcss.config.js
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};Drie bestanden teruggebracht naar één regel. Dat is niet alleen minder boilerplate — het is minder oppervlak voor verkeerde configuratie. In v4 is contentdetectie automatisch. Het scant je projectbestanden zonder dat je glob-patronen hoeft op te geven.
CSS-First Configuratie met @theme#
Dit is de grootste conceptuele verschuiving. In v3 paste je Tailwind aan via een JavaScript-configuratie-object:
// v3 — tailwind.config.js
module.exports = {
theme: {
extend: {
colors: {
brand: {
50: "#eff6ff",
500: "#3b82f6",
900: "#1e3a5f",
},
},
fontFamily: {
display: ["Inter Variable", "sans-serif"],
},
spacing: {
18: "4.5rem",
112: "28rem",
},
borderRadius: {
"4xl": "2rem",
},
},
},
};In v4 leeft dit allemaal in CSS met de @theme-directive:
@import "tailwindcss";
@theme {
--color-brand-50: #eff6ff;
--color-brand-500: #3b82f6;
--color-brand-900: #1e3a5f;
--font-display: "Inter Variable", sans-serif;
--spacing-18: 4.5rem;
--spacing-112: 28rem;
--radius-4xl: 2rem;
}In het begin verzette ik me hiertegen. Ik vond het fijn om een enkel JavaScript-object te hebben waar ik mijn hele design system kon zien. Maar na een week met de CSS-aanpak veranderde ik van gedachten om drie redenen:
1. Native CSS custom properties worden automatisch blootgesteld. Elke waarde die je in @theme definieert wordt een CSS custom property op :root. Dat betekent dat je themawaarden toegankelijk zijn in gewone CSS, in CSS Modules, in <style> tags, overal waar CSS draait:
/* je krijgt dit gratis */
:root {
--color-brand-50: #eff6ff;
--color-brand-500: #3b82f6;
--color-brand-900: #1e3a5f;
}/* gebruik ze overal — geen Tailwind nodig */
.custom-element {
border: 2px solid var(--color-brand-500);
}2. Je kunt CSS-features gebruiken binnen @theme. Media queries, light-dark(), calc() — echte CSS werkt hier omdat het echte CSS is:
@theme {
--color-surface: light-dark(#ffffff, #0a0a0a);
--color-text: light-dark(#0a0a0a, #fafafa);
--spacing-container: calc(100vw - 2rem);
}3. Co-locatie met je andere CSS. Je thema, je custom utilities en je basisstijlen leven allemaal in dezelfde taal, in hetzelfde bestand als je wilt. Er is geen context-switching meer tussen "CSS-wereld" en "JavaScript-config-wereld."
Overschrijven vs. Uitbreiden van het Standaardthema#
In v3 had je theme (vervangen) vs theme.extend (samenvoegen). In v4 is het mentale model anders:
@import "tailwindcss";
/* Dit BREIDT het standaardthema UIT — voegt merkkleren toe naast bestaande */
@theme {
--color-brand-500: #3b82f6;
}Als je een complete namespace wilt vervangen (zoals alle standaardkleuren verwijderen), gebruik je @theme met de --color-* wildcard reset:
@import "tailwindcss";
@theme {
/* Wis eerst alle standaardkleuren */
--color-*: initial;
/* Definieer nu alleen jouw kleuren */
--color-white: #ffffff;
--color-black: #000000;
--color-brand-50: #eff6ff;
--color-brand-500: #3b82f6;
--color-brand-900: #1e3a5f;
}Dit wildcard-resetpatroon is elegant. Je kiest precies welke delen van het standaardthema je behoudt en welke je vervangt. Wil je alle standaard spacing maar aangepaste kleuren? Reset --color-*: initial; en laat spacing met rust.
Meerdere Themabestanden#
Voor grotere projecten kun je je thema over bestanden verdelen:
/* styles/theme/colors.css */
@theme {
--color-brand-50: #eff6ff;
--color-brand-100: #dbeafe;
--color-brand-200: #bfdbfe;
--color-brand-300: #93c5fd;
--color-brand-400: #60a5fa;
--color-brand-500: #3b82f6;
--color-brand-600: #2563eb;
--color-brand-700: #1d4ed8;
--color-brand-800: #1e40af;
--color-brand-900: #1e3a5f;
--color-brand-950: #172554;
}
/* styles/theme/typography.css */
@theme {
--font-display: "Inter Variable", sans-serif;
--font-body: "Source Sans 3 Variable", sans-serif;
--font-mono: "JetBrains Mono Variable", monospace;
--text-display: 3.5rem;
--text-display--line-height: 1.1;
--text-display--letter-spacing: -0.02em;
}/* app.css */
@import "tailwindcss";
@import "./theme/colors.css";
@import "./theme/typography.css";Dit is veel schoner dan het v3-patroon van een gigantische tailwind.config.js of het proberen op te splitsen met require().
De Oxide Engine: Het Is Echt 10x Sneller#
De engine van Tailwind v4 is een complete herschrijving in Rust. Ze noemen het Oxide. Ik was sceptisch over de "10x sneller" claim — marketingcijfers overleven zelden het contact met echte projecten. Dus ik heb het gemeten.
Mijn testproject: Een Next.js-app met 847 componenten, 142 pagina's, ongeveer 23.000 Tailwind class-gebruiken.
| Metric | v3 (Node) | v4 (Oxide) | Verbetering |
|---|---|---|---|
| Initiële build | 4.280ms | 387ms | 11x |
| Incrementeel (1 bestand bewerken) | 340ms | 18ms | 19x |
| Volledige rebuild (schoon) | 5.100ms | 510ms | 10x |
| Dev server start | 3.200ms | 290ms | 11x |
De "10x" claim is conservatief voor mijn project. Incrementele builds zijn waar het echt schittert — 18ms betekent dat het in wezen instant is. Je slaat een bestand op en de browser heeft de nieuwe stijlen voordat je van tab kunt wisselen.
Waarom Is Het Zoveel Sneller?#
Drie redenen:
1. Rust in plaats van JavaScript. De kern-CSS-parser, class-detectie en codegeneratie zijn allemaal native Rust. Dit is geen "laten we herschrijven in Rust voor de lol" situatie — CSS-parsing is oprecht CPU-gebonden werk waar native code een enorm voordeel heeft boven V8.
2. Geen PostCSS in het hot path. In v3 was Tailwind een PostCSS-plugin. Elke build betekende: parse CSS naar PostCSS AST, draai Tailwind-plugin, serialiseer terug naar CSS-string, dan draaien andere PostCSS-plugins. In v4 heeft Tailwind zijn eigen CSS-parser die direct van bron naar output gaat. PostCSS wordt nog steeds ondersteund voor compatibiliteit, maar het primaire pad slaat het volledig over.
3. Slimmere incrementele verwerking. De nieuwe engine cachet agressief. Wanneer je een enkel bestand bewerkt, herscant het alleen dat bestand op class-namen en regenereert het alleen de CSS-regels die veranderden. De v3-engine was hier al slimmer in dan mensen denken (JIT-modus was al incrementeel), maar v4 gaat veel verder met fijnmazige dependency-tracking.
Maakt Snelheid Eigenlijk Uit?#
Ja, maar niet om de reden die je zou verwachten. Voor de meeste projecten was de buildsnelheid van v3 "prima." Je wachtte een paar honderd milliseconden in dev. Niet pijnlijk.
De snelheid van v4 is belangrijk omdat het Tailwind onzichtbaar maakt in je toolchain. Wanneer builds onder de 20ms zijn, stop je met nadenken over Tailwind als buildstap. Het wordt als syntax highlighting — altijd aanwezig, nooit in de weg. Dat psychologische verschil is significant over een volle dag development.
Native @layer Integratie#
In v3 gebruikte Tailwind zijn eigen layer-systeem met @layer base, @layer components en @layer utilities. Deze leken op CSS cascade layers maar waren het niet — het waren Tailwind-specifieke directives die bepaalden waar gegenereerde CSS in de output verscheen.
In v4 gebruikt Tailwind daadwerkelijke CSS cascade layers:
/* v4 output — vereenvoudigd */
@layer theme, base, components, utilities;
@layer base {
/* reset, preflight */
}
@layer components {
/* je component-classes */
}
@layer utilities {
/* alle gegenereerde utility-classes */
}Dit is een significante verandering omdat CSS cascade layers echte specificiteitsimplicaties hebben. Een regel in een layer met lagere prioriteit verliest altijd van een regel in een layer met hogere prioriteit, ongeacht selectorspecificiteit. Dat betekent:
@layer components {
/* specificiteit: 0-1-0 */
.card { padding: 1rem; }
}
@layer utilities {
/* specificiteit: 0-1-0 — zelfde specificiteit maar wint omdat utilities layer later komt */
.p-4 { padding: 1rem; }
}Utilities overschrijven altijd components. Components overschrijven altijd base. Dit is hoe Tailwind conceptueel werkte in v3, maar nu wordt het afgedwongen door het cascade layer-mechanisme van de browser, niet door manipulatie van bronvolgorde.
Custom Utilities Toevoegen#
In v3 definieerde je custom utilities met een plugin-API of @layer utilities:
// v3 — plugin-aanpak
const plugin = require("tailwindcss/plugin");
module.exports = {
plugins: [
plugin(function ({ addUtilities }) {
addUtilities({
".text-balance": {
"text-wrap": "balance",
},
".text-pretty": {
"text-wrap": "pretty",
},
});
}),
],
};In v4 worden custom utilities gedefinieerd met de @utility-directive:
@import "tailwindcss";
@utility text-balance {
text-wrap: balance;
}
@utility text-pretty {
text-wrap: pretty;
}De @utility-directive vertelt Tailwind "dit is een utility-class — zet het in de utilities-layer en sta toe dat het met variants gebruikt wordt." Dat laatste is cruciaal. Een utility gedefinieerd met @utility werkt automatisch met hover:, focus:, md: en elke andere variant:
<p class="text-pretty md:text-balance">...</p>Custom Variants#
Je kunt ook custom variants definiëren met @variant:
@import "tailwindcss";
@variant hocus (&:hover, &:focus);
@variant theme-dark (.dark &);<button class="hocus:bg-brand-500 theme-dark:text-white">
Click me
</button>Dit vervangt de v3 addVariant plugin-API voor de meeste use cases. Het is minder krachtig (je kunt geen programmatische variant-generatie doen), maar het dekt 90% van wat mensen daadwerkelijk doen.
Container Queries: Ingebouwd, Geen Plugin#
Container queries waren een van de meest gevraagde features in v3. Je kon ze krijgen met de @tailwindcss/container-queries plugin, maar het was een add-on. In v4 zijn ze ingebouwd in het framework.
Basisgebruik#
Markeer een container met @container en bevraag de grootte met het @-prefix:
<!-- markeer parent als container -->
<div class="@container">
<!-- responsive ten opzichte van de breedte van de parent, niet de viewport -->
<div class="flex flex-col @md:flex-row @lg:grid @lg:grid-cols-3">
<div class="p-4">Card 1</div>
<div class="p-4">Card 2</div>
<div class="p-4">Card 3</div>
</div>
</div>De @md, @lg, etc. variants werken als responsive breakpoints maar zijn relatief ten opzichte van de dichtstbijzijnde @container-voorouder in plaats van de viewport. De breakpointwaarden komen overeen met de standaard breakpoints van Tailwind:
| Variant | Min-breedte |
|---|---|
@sm | 24rem (384px) |
@md | 28rem (448px) |
@lg | 32rem (512px) |
@xl | 36rem (576px) |
@2xl | 42rem (672px) |
Benoemde Containers#
Je kunt containers benoemen om specifieke voorouders te bevragen:
<div class="@container/sidebar">
<div class="@container/card">
<!-- bevraagt de card-container -->
<div class="@md/card:text-lg">...</div>
<!-- bevraagt de sidebar-container -->
<div class="@lg/sidebar:hidden">...</div>
</div>
</div>Waarom Dit Ertoe Doet#
Container queries veranderen hoe je denkt over responsive design. In plaats van "bij deze viewport-breedte, toon drie kolommen," zeg je "wanneer de container van dit component breed genoeg is, toon drie kolommen." Componenten worden echt zelfstandig. Je kunt een card-component verplaatsen van een full-width layout naar een sidebar en het past zich automatisch aan. Geen media query-gymnastiek.
Ik ben mijn componentbibliotheken aan het refactoren om standaard container queries te gebruiken in plaats van viewport-breakpoints. Het resultaat zijn componenten die werken waar je ze ook plaatst, zonder dat de parent iets hoeft te weten over het responsive gedrag van het component.
<!-- Dit component past zich aan ELKE container aan waarin het geplaatst wordt -->
<article class="@container">
<div class="grid grid-cols-1 @md:grid-cols-[200px_1fr] gap-4">
<img
class="w-full @md:w-auto rounded-lg aspect-video @md:aspect-square object-cover"
src="/post-image.jpg"
alt=""
/>
<div>
<h2 class="text-lg @lg:text-xl font-semibold">Post Title</h2>
<p class="mt-2 text-sm @md:text-base text-gray-600">
Post excerpt goes here...
</p>
<div class="mt-4 hidden @md:flex gap-2">
<span class="text-xs bg-gray-100 px-2 py-1 rounded">Tag</span>
</div>
</div>
</div>
</article>Nieuwe Variants Die Er Echt Toe Doen#
v4 voegt meerdere nieuwe variants toe waar ik constant naar grijp. Ze vullen echte hiaten.
De starting: Variant#
Deze mapt naar CSS @starting-style, waarmee je de initiële staat van een element kunt definiëren wanneer het voor het eerst verschijnt. Dit is het ontbrekende puzzelstuk voor het animeren van element-entry zonder JavaScript:
<dialog class="opacity-0 starting:opacity-0 open:opacity-100 transition-opacity duration-300">
<p>This dialog fades in when opened</p>
</dialog>De starting:-variant genereert CSS binnen een @starting-style-blok:
/* wat Tailwind genereert */
@starting-style {
dialog[open] {
opacity: 0;
}
}
dialog[open] {
opacity: 1;
transition: opacity 300ms;
}Dit is enorm voor dialogs, popovers, dropdown-menu's — alles dat een entry-animatie nodig heeft. Hiervoor had je JavaScript nodig om een class toe te voegen op het volgende frame, of je gebruikte @keyframes. Nu is het een utility-class.
De not-* Variant#
Negatie. Iets wat we al eeuwen wilden:
<!-- elk child behalve de laatste krijgt een border -->
<div class="divide-y">
<div class="not-last:pb-4">Item 1</div>
<div class="not-last:pb-4">Item 2</div>
<div class="not-last:pb-4">Item 3</div>
</div>
<!-- style alles dat niet disabled is -->
<input class="not-disabled:hover:border-brand-500" />
<!-- negeer data-attributen -->
<div class="not-data-active:opacity-50">...</div>De nth-* Variants#
Directe nth-child en nth-of-type toegang:
<ul>
<li class="nth-1:font-bold">Eerste item — vet</li>
<li class="nth-even:bg-gray-50">Even rijen — grijze achtergrond</li>
<li class="nth-odd:bg-white">Oneven rijen — witte achtergrond</li>
<li class="nth-[3n+1]:text-brand-500">Elke derde+1 — merkkleur</li>
</ul>De bracket-syntax (nth-[3n+1]) ondersteunt elke geldige nth-child-expressie. Dit vervangt een hoop aangepaste CSS die ik vroeger schreef voor tabelstriping en grid-patronen.
De in-* Variant (Parent State)#
Dit is het omgekeerde van group-*. In plaats van "wanneer mijn parent (group) gehoverd wordt, style mij," is het "wanneer ik me binnen een parent bevind die deze state matcht, style mij":
<div class="in-data-active:bg-brand-50">
Dit krijgt een achtergrond wanneer een voorouder data-active heeft
</div>De **: Deep Universal Variant#
Style alle nakomelingen, niet alleen directe children. Dit is gecontroleerde kracht — gebruik het spaarzaam, maar het is onmisbaar voor prosacontent en CMS-output:
<!-- alle paragrafen in deze div, op elke diepte -->
<div class="**:data-highlight:bg-yellow-100">
<section>
<p data-highlight>Dit wordt gehighlight</p>
<div>
<p data-highlight>Dit ook, dieper genest</p>
</div>
</section>
</div>Breaking Changes: Wat Er Echt Brak#
Laat me eerlijk zijn. Als je een groot v3-project hebt, is migratie niet triviaal. Dit is wat er brak in mijn projecten:
1. Configuratieformaat#
Je tailwind.config.js werkt niet direct. Je moet ofwel:
- Het converteren naar
@themeCSS (aanbevolen voor de nieuwe architectuur) - De compatibiliteitslaag
@config-directive gebruiken (snelle migratieroute)
/* snelle migratie — behoud je oude config */
@import "tailwindcss";
@config "../../tailwind.config.js";Deze @config-brug werkt, maar het is expliciet een migratietool. De aanbeveling is om na verloop van tijd naar @theme over te stappen.
2. Verwijderde Verouderde Utilities#
Sommige utilities die deprecated waren in v3 zijn verdwenen:
/* VERWIJDERD in v4 */
bg-opacity-* → gebruik bg-black/50 (slash-opacity syntax)
text-opacity-* → gebruik text-black/50
border-opacity-* → gebruik border-black/50
flex-shrink-* → gebruik shrink-*
flex-grow-* → gebruik grow-*
overflow-ellipsis → gebruik text-ellipsis
decoration-slice → gebruik box-decoration-slice
decoration-clone → gebruik box-decoration-clone
Als je al de moderne syntax in v3 gebruikte (slash-opacity, shrink-*), zit je goed. Zo niet, dan zijn dit eenvoudige zoek-en-vervang wijzigingen.
3. Wijzigingen in het Standaard Kleurenpalet#
Het standaard kleurenpalet is licht verschoven. Als je afhankelijk bent van exacte kleurwaarden uit v3 (niet op naam maar op de daadwerkelijke hexwaarde), merk je misschien visuele verschillen. De benoemde kleuren (blue-500, gray-200) bestaan nog steeds maar sommige hexwaarden zijn veranderd.
4. Contentdetectie#
v3 vereiste expliciete content-configuratie:
// v3
module.exports = {
content: [
"./src/**/*.{js,ts,jsx,tsx}",
"./components/**/*.{js,ts,jsx,tsx}",
],
};v4 gebruikt automatische contentdetectie. Het scant je projectroot en vindt templatebestanden automatisch. Dit "werkt gewoon" meestal, maar als je een ongebruikelijke projectstructuur hebt (monorepo met packages buiten de projectroot, templatebestanden op onverwachte locaties), moet je bronpaden mogelijk expliciet configureren:
@import "tailwindcss";
@source "../shared-components/**/*.tsx";
@source "../design-system/src/**/*.tsx";5. Plugin API-wijzigingen#
Als je custom plugins hebt geschreven, is de API veranderd. De addUtilities, addComponents, addBase en addVariant functies werken nog via de compatibiliteitslaag, maar de idiomatische v4-aanpak is CSS-native:
// v3 plugin
plugin(function ({ addUtilities, theme }) {
addUtilities({
".scrollbar-hide": {
"-ms-overflow-style": "none",
"scrollbar-width": "none",
"&::-webkit-scrollbar": {
display: "none",
},
},
});
});/* v4 — gewoon CSS */
@utility scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}De meeste first-party plugins (@tailwindcss/typography, @tailwindcss/forms, @tailwindcss/aspect-ratio) hebben v4-compatibele versies. Third-party plugins zijn wisselend — controleer hun repo voordat je migreert.
6. JIT Is de Enige Modus#
In v3 kon je afzien van JIT-modus (hoewel bijna niemand dat deed). In v4 is er geen niet-JIT modus. Alles wordt on-demand gegenereerd, altijd. Als je om een reden de oude AOT (ahead-of-time) engine gebruikte, is dat pad verdwenen.
7. Sommige Variant-syntaxwijzigingen#
Een paar variants zijn hernoemd of hebben ander gedrag gekregen:
<!-- v3 -->
<div class="[&>*]:p-4">...</div>
<!-- v4 — het >* deel gebruikt nu de inset variant-syntax -->
<div class="*:p-4">...</div>De willekeurige variant-syntax [&...] werkt nog steeds, maar v4 biedt benoemde alternatieven voor veelvoorkomende patronen.
Migratiegids: Het Echte Proces#
Hier is hoe ik daadwerkelijk migreerde, niet het happy path uit de docs maar hoe het proces er echt uitzag.
Stap 1: Draai de Officiële Codemod#
Tailwind biedt een codemod die de meeste mechanische wijzigingen afhandelt:
npx @tailwindcss/upgradeDit doet veel automatisch:
- Converteert
@tailwind-directives naar@import "tailwindcss" - Hernoemt verouderde utility-classes
- Werkt variant-syntax bij
- Converteert opacity-utilities naar slash-syntax (
bg-opacity-50naarbg-black/50) - Maakt een basis
@theme-blok van je config
Wat de Codemod Goed Afhandelt#
- Utility class-hernoemingen (bijna perfect)
- Directive-syntaxwijzigingen
- Simpele themawaarden (kleuren, spacing, fonts)
- Opacity-syntaxmigratie
Wat de Codemod NIET Afhandelt#
- Complexe plugin-conversies
- Dynamische config-waarden (
theme()-aanroepen in JavaScript) - Conditionele themaconfiguratie (bijv. themawaarden op basis van omgeving)
- Custom plugin API-migraties
- Edge cases met arbitraire waarden waar de nieuwe parser anders interpreteert
- Class-namen die dynamisch worden geconstrueerd in JavaScript (template literals, string-concatenatie)
Stap 2: Fix PostCSS-configuratie#
Voor de meeste setups update je je PostCSS-config:
// postcss.config.js — v4
module.exports = {
plugins: {
"@tailwindcss/postcss": {},
},
};Let op: de pluginnaam is veranderd van tailwindcss naar @tailwindcss/postcss. Als je Vite gebruikt, kun je PostCSS volledig overslaan en de Vite-plugin gebruiken:
// vite.config.ts
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
plugins: [tailwindcss()],
});Stap 3: Converteer Themaconfiguratie#
Dit is het handmatige deel. Neem je tailwind.config.js themawaarden en converteer ze naar @theme:
// v3 config — voor
module.exports = {
theme: {
extend: {
colors: {
brand: {
light: "#60a5fa",
DEFAULT: "#3b82f6",
dark: "#1d4ed8",
},
},
fontSize: {
"2xs": ["0.65rem", { lineHeight: "1rem" }],
},
animation: {
"fade-in": "fade-in 0.5s ease-out",
},
keyframes: {
"fade-in": {
"0%": { opacity: "0" },
"100%": { opacity: "1" },
},
},
},
},
};/* v4 CSS — na */
@import "tailwindcss";
@theme {
--color-brand-light: #60a5fa;
--color-brand: #3b82f6;
--color-brand-dark: #1d4ed8;
--text-2xs: 0.65rem;
--text-2xs--line-height: 1rem;
--animate-fade-in: fade-in 0.5s ease-out;
}
@keyframes fade-in {
0% { opacity: 0; }
100% { opacity: 1; }
}Merk op dat keyframes buiten @theme verhuizen en gewone CSS @keyframes worden. De animatienaam in @theme verwijst er alleen naar. Dit is schoner — keyframes zijn CSS, ze moeten als CSS geschreven worden.
Stap 4: Visuele Regressietests#
Dit is niet onderhandelbaar. Na migratie opende ik elke pagina van mijn app en controleerde het visueel. Ik draaide ook mijn Playwright screenshot-tests (als je die hebt). De codemod is goed maar niet perfect. Dingen die ik opving in visuele review:
- Een paar plekken waar opacity-syntaxmigratie licht afwijkende resultaten opleverde
- Custom plugin-output die niet meekwam
- Z-index stacking-wijzigingen door layer-volgorde
- Sommige
!important-overrides die anders gedroegen met cascade layers
Stap 5: Update Third-Party Dependencies#
Controleer elk Tailwind-gerelateerd package:
{
"@tailwindcss/typography": "^1.0.0",
"@tailwindcss/forms": "^1.0.0",
"@tailwindcss/container-queries": "VERWIJDEREN — nu ingebouwd",
"tailwindcss-animate": "controleer v4-ondersteuning",
"prettier-plugin-tailwindcss": "update naar laatste versie"
}De @tailwindcss/container-queries plugin is niet meer nodig — container queries zijn ingebouwd. Andere plugins hebben hun v4-compatibele versies nodig.
Werken met Next.js#
Omdat ik Next.js voor de meeste projecten gebruik, hier de specifieke setup.
PostCSS-aanpak (Aanbevolen voor Next.js)#
Next.js gebruikt PostCSS onder de motorkap, dus de PostCSS-plugin is de logische keuze:
npm install tailwindcss @tailwindcss/postcss// postcss.config.mjs
export default {
plugins: {
"@tailwindcss/postcss": {},
},
};/* app/globals.css */
@import "tailwindcss";
@theme {
--font-sans: "Inter Variable", ui-sans-serif, system-ui, sans-serif;
--font-mono: "JetBrains Mono Variable", ui-monospace, monospace;
}Dat is de complete setup. Geen tailwind.config.js, geen autoprefixer (v4 handelt vendor prefixes intern af).
CSS Import-volgorde#
Eén ding dat me in de war bracht: CSS import-volgorde is belangrijker in v4 vanwege cascade layers. Je @import "tailwindcss" moet voor je custom stijlen komen:
/* correcte volgorde */
@import "tailwindcss";
@import "./theme.css";
@import "./custom-utilities.css";
/* je inline @theme, @utility, etc. */Als je custom CSS importeert voor Tailwind, kunnen je stijlen in een lagere cascade layer terechtkomen en onverwacht overschreven worden.
Dark Mode#
Dark mode werkt conceptueel hetzelfde maar de configuratie is verhuisd naar CSS:
@import "tailwindcss";
/* Gebruik class-gebaseerde dark mode (standaard is media-gebaseerd) */
@variant dark (&:where(.dark, .dark *));Dit vervangt de v3-config:
// v3
module.exports = {
darkMode: "class",
};De @variant-aanpak is flexibeler. Je kunt dark mode definiëren hoe je wilt — class-gebaseerd, data-attribuut-gebaseerd, of media-query-gebaseerd:
/* data-attribuut aanpak */
@variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));
/* media query — dit is de standaard, dus je hoeft het niet te declareren */
@variant dark (@media (prefers-color-scheme: dark));Turbopack-compatibiliteit#
Als je Next.js met Turbopack gebruikt (wat nu de standaard dev-bundler is), werkt v4 uitstekend. De Rust-engine sluit goed aan bij Turbopack's eigen op Rust gebaseerde architectuur. Ik heb dev-opstarttijden gemeten:
| Setup | v3 + Webpack | v3 + Turbopack | v4 + Turbopack |
|---|---|---|---|
| Koude start | 4,8s | 2,1s | 1,3s |
| HMR (CSS-wijziging) | 450ms | 180ms | 40ms |
De 40ms HMR voor CSS-wijzigingen is nauwelijks waarneembaar. Het voelt instant.
Performance Deep Dive: Voorbij Buildsnelheid#
De voordelen van de Oxide-engine gaan verder dan ruwe buildsnelheid.
Geheugengebruik#
v4 gebruikt significant minder geheugen. Op mijn project met 847 componenten:
| Metric | v3 | v4 |
|---|---|---|
| Piekgeheugen (build) | 380MB | 45MB |
| Stabiel (dev) | 210MB | 28MB |
Dit is belangrijk voor CI/CD-pipelines waar geheugen beperkt is, en voor development-machines die tien processen tegelijk draaien.
CSS Output-grootte#
v4 genereert iets kleinere CSS-output omdat de nieuwe engine beter is in deduplicatie en dead code elimination:
v3 output: 34,2 KB (gzipped)
v4 output: 29,8 KB (gzipped)
Een reductie van 13% zonder code te wijzigen. Niet transformatief, maar gratis performance.
Tree Shaking van Themawaarden#
In v4 wordt, als je een themawaarde definieert maar nooit in je templates gebruikt, de bijbehorende CSS custom property nog steeds uitgegeven (het staat in @theme, wat mapt naar :root-variabelen). Echter, de utility-classes voor ongebruikte waarden worden niet gegenereerd. Dit is hetzelfde als v3's JIT-gedrag maar het vermelden waard: je CSS custom properties zijn altijd beschikbaar, zelfs voor waarden zonder utility-gebruik.
Als je wilt voorkomen dat bepaalde themawaarden CSS custom properties genereren, kun je @theme inline gebruiken:
@theme inline {
/* Deze waarden genereren utilities maar GEEN CSS custom properties */
--color-internal-debug: #ff00ff;
--spacing-magic-number: 3.7rem;
}Dit is handig voor interne design tokens die je niet als CSS-variabelen wilt blootstellen.
Geavanceerd: Thema's Samenstellen voor Multi-Brand#
Eén patroon dat v4 significant makkelijker maakt is multi-brand theming. Omdat themawaarden CSS custom properties zijn, kun je ze op runtime wisselen:
@import "tailwindcss";
@theme {
--color-brand: var(--brand-primary, #3b82f6);
--color-brand-light: var(--brand-light, #60a5fa);
--color-brand-dark: var(--brand-dark, #1d4ed8);
}
/* Brand-overrides */
.theme-acme {
--brand-primary: #e11d48;
--brand-light: #fb7185;
--brand-dark: #9f1239;
}
.theme-globex {
--brand-primary: #059669;
--brand-light: #34d399;
--brand-dark: #047857;
}<body class="theme-acme">
<!-- alle bg-brand, text-brand, etc. gebruiken Acme-kleuren -->
<div class="bg-brand text-white">Acme Corp</div>
</body>In v3 vereiste dit een custom plugin of complexe CSS-variabele setup buiten Tailwind. In v4 is het natuurlijk — het thema is CSS-variabelen, en CSS-variabelen cascaderen. Dit is het soort ding dat de CSS-first aanpak juist doet aanvoelen.
Wat Ik Mis van v3#
Laat me eerlijk zijn. Er zijn dingen die v3 deed die ik oprecht mis in v4:
1. JavaScript-config voor programmatische thema's. Ik had een project waar we kleurschalen genereerden vanuit een enkele merkkleur met een JavaScript-functie in de config. In v4 kun je dat niet doen in @theme — je zou een buildstap nodig hebben die het CSS-bestand genereert, of je berekent de kleuren eenmalig en plakt ze erin. De @config-compatibiliteitslaag helpt, maar het is niet het langetermijnverhaal.
2. IntelliSense was beter bij de lancering. De v3 VS Code-extensie had jaren van fijnslijpen. v4 IntelliSense werkt maar had vroeg wat hiaten — custom @theme-waarden werden soms niet ge-autocomplete, en @utility-definities werden niet altijd opgepikt. Dit is aanzienlijk verbeterd met recente updates, maar het is het vermelden waard.
3. Ecosysteemvolwassenheid. Het ecosysteem rond v3 was enorm. Headless UI, Radix, shadcn/ui, Flowbite, DaisyUI — alles was getest tegen v3. v4-ondersteuning wordt uitgerold maar is niet universeel. Ik moest een PR indienen bij één componentbibliotheek om v4-compatibiliteit te fixen.
Moet Je Migreren?#
Hier is mijn besliskader na enkele weken met v4 te hebben geleefd:
Migreer Nu Als:#
- Je een nieuw project begint (voor de hand liggende keuze — begin met v4)
- Je project minimale custom plugins heeft
- Je de prestatievoordelen wilt voor grote projecten
- Je al moderne Tailwind-patronen gebruikt (slash-opacity,
shrink-*, etc.) - Je container queries nodig hebt en liever geen plugin toevoegt
Wacht Als:#
- Je sterk afhankelijk bent van third-party Tailwind-plugins die v4 nog niet ondersteunen
- Je complexe programmatische themaconfiguratie hebt
- Je project stabiel is en niet actief wordt ontwikkeld (waarom eraan komen?)
- Je midden in een feature-sprint zit (migreer tussen sprints, niet tijdens)
Migreer Niet Als:#
- Je op v2 of eerder zit (upgrade eerst naar v3, stabiliseer, overweeg dan v4)
- Je project binnen een paar maanden eindigt (niet de moeite van de churn waard)
Mijn Eerlijke Mening#
Voor nieuwe projecten is v4 de voor de hand liggende keuze. De CSS-first configuratie is schoner, de engine is dramatisch sneller, en de nieuwe features (container queries, @starting-style, nieuwe variants) zijn oprecht nuttig.
Voor bestaande projecten raad ik een gefaseerde aanpak aan:
- Nu: Begin elk nieuw project op v4
- Binnenkort: Experimenteer door een klein intern project naar v4 te converteren
- Wanneer klaar: Migreer productieprojecten tijdens een rustige sprint, met visuele regressietests
De migratie is niet pijnlijk als je je erop voorbereidt. De codemod handelt 80% van het werk af. De resterende 20% is handmatig maar eenvoudig. Reken een dag voor een middelgroot project, twee tot drie dagen voor een groot project.
Tailwind v4 is wat Tailwind al die tijd had moeten zijn. De JavaScript-configuratie was altijd een concessie aan de tooling van die tijd. CSS-first configuratie, native cascade layers, een Rust-engine — dit zijn geen trends, het is het framework dat het platform inhaalt. Het webplatform werd beter, en Tailwind v4 leunt erin in plaats van ertegen te vechten.
De stap om je design tokens in CSS te schrijven, ze samen te stellen met CSS-features, en de eigen cascade van de browser de specificiteit te laten afhandelen — dat is de juiste richting. Het duurde vier major versies om hier te komen, maar het resultaat is de meest coherente versie van Tailwind tot nu toe.
Begin je volgende project ermee. Je kijkt niet meer terug.