Tailwind CSS v4: Vad som faktiskt ändrades och om du bör migrera
CSS-first-konfiguration, @layer-integration, inbyggda container queries, den nya motorns prestanda, breaking changes och min ärliga migrationsupplevelse från v3 till v4.
Jag har använt Tailwind CSS sedan v1.x, redan när halva communityn tyckte det var en styggelse och den andra halvan inte kunde sluta leverera med det. Varje stor version har varit ett betydande kliv, men v4 är annorlunda. Det är inte bara en funktionsrelease. Det är en omskrivning från grunden som ändrar det grundläggande kontraktet mellan dig och ramverket.
Efter att ha migrerat två produktionsprojekt från v3 till v4 och startat tre nya projekt på v4 från scratch har jag en tydlig bild av vad som genuint är bättre, vad som är ruffigt, och om du bör migrera idag. Ingen hype, ingen upprördhet — bara vad jag observerade.
Helhetsbilden: Vad v4 faktiskt är#
Tailwind CSS v4 är tre saker på en gång:
- En ny motor — omskriven från JavaScript till Rust (Oxide-motorn), vilket gör byggen dramatiskt snabbare
- Ett nytt konfigurationsparadigm — CSS-first-konfiguration ersätter
tailwind.config.jssom standard - En tätare integration med CSS-plattformen — nativt
@layer, container queries,@starting-styleoch kaskadlager är förstaklassens medborgare
Rubriken du ser överallt är "10x snabbare." Det stämmer, men det undersäljer den faktiska förändringen. Den mentala modellen för att konfigurera och utöka Tailwind har fundamentalt skiftat. Du arbetar med CSS nu, inte ett JavaScript-konfigurationsobjekt som genererar CSS.
Så här ser en minimal Tailwind v4-setup ut:
/* app.css — detta är hela setupen */
@import "tailwindcss";Det är allt. Ingen konfigfil. Ingen PostCSS-pluginkonfiguration (för de flesta uppsättningar). Inga @tailwind base; @tailwind components; @tailwind utilities;-direktiv. En import, och du kör.
Jämför det med 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: {},
},
};Tre filer reducerade till en rad. Det är inte bara mindre boilerplate — det är mindre yta för felkonfiguration. I v4 är innehållsdetektering automatisk. Den skannar dina projektfiler utan att du behöver stava ut glob-mönster.
CSS-First-konfiguration med @theme#
Detta är det största konceptuella skiftet. I v3 anpassade du Tailwind genom ett JavaScript-konfigurationsobjekt:
// 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",
},
},
},
};I v4 lever allt detta i CSS med hjälp av @theme-direktivet:
@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;
}Till en början motsatte jag mig detta. Jag gillade att ha ett enda JavaScript-objekt där jag kunde se hela mitt designsystem. Men efter en vecka med CSS-metoden ändrade jag mig av tre skäl:
1. Nativa CSS custom properties exponeras automatiskt. Varje värde du definierar i @theme blir en CSS custom property på :root. Det betyder att dina temavärden är tillgängliga i ren CSS, i CSS Modules, i <style>-taggar, överallt där CSS körs:
/* du får detta gratis */
:root {
--color-brand-50: #eff6ff;
--color-brand-500: #3b82f6;
--color-brand-900: #1e3a5f;
}/* använd dem var som helst — inget Tailwind behövs */
.custom-element {
border: 2px solid var(--color-brand-500);
}2. Du kan använda CSS-funktioner inuti @theme. Media queries, light-dark(), calc() — riktig CSS fungerar här eftersom det är riktig CSS:
@theme {
--color-surface: light-dark(#ffffff, #0a0a0a);
--color-text: light-dark(#0a0a0a, #fafafa);
--spacing-container: calc(100vw - 2rem);
}3. Samlokalisering med din övriga CSS. Ditt tema, dina anpassade verktyg och dina basstilar lever alla på samma språk, i samma fil om du vill. Det finns ingen kontextväxling mellan "CSS-världen" och "JavaScript-konfigurationsvärlden."
Överskrivning vs. utökning av standardtemat#
I v3 hade du theme (ersätt) vs theme.extend (slå samman). I v4 är den mentala modellen annorlunda:
@import "tailwindcss";
/* Detta UTÖKAR standardtemat — lägger till varumärkesfärger vid sidan av befintliga */
@theme {
--color-brand-500: #3b82f6;
}Om du vill ersätta ett namnområde helt (som att ta bort alla standardfärger), använder du @theme med --color-* wildcard-återställning:
@import "tailwindcss";
@theme {
/* Rensa alla standardfärger först */
--color-*: initial;
/* Definiera nu bara dina färger */
--color-white: #ffffff;
--color-black: #000000;
--color-brand-50: #eff6ff;
--color-brand-500: #3b82f6;
--color-brand-900: #1e3a5f;
}Detta wildcard-återställningsmönster är elegant. Du väljer exakt vilka delar av standardtemat du vill behålla och vilka du vill ersätta. Vill du ha alla standardavstånd men anpassade färger? Återställ --color-*: initial; och lämna avstånd ifred.
Flera temafiler#
För större projekt kan du dela upp ditt tema över filer:
/* 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";Detta är mycket renare än v3-mönstret att ha en gigantisk tailwind.config.js eller försöka dela upp den med require().
Oxide-motorn: Den är faktiskt 10x snabbare#
Tailwind v4:s motor är en fullständig omskrivning i Rust. De kallar den Oxide. Jag var skeptisk till påståendet "10x snabbare" — marknadsföringssiffror överlever sällan kontakt med riktiga projekt. Så jag benchmarkade det.
Mitt testprojekt: En Next.js-app med 847 komponenter, 142 sidor, ungefär 23 000 Tailwind-klassanvändningar.
| Mätvärde | v3 (Node) | v4 (Oxide) | Förbättring |
|---|---|---|---|
| Initial build | 4 280ms | 387ms | 11x |
| Inkrementell (redigera 1 fil) | 340ms | 18ms | 19x |
| Full rebuild (ren) | 5 100ms | 510ms | 10x |
| Dev server-start | 3 200ms | 290ms | 11x |
Påståendet "10x" är konservativt för mitt projekt. Inkrementella byggen är där det verkligen lyser — 18ms innebär att det i princip är omedelbart. Du sparar en fil och webbläsaren har de nya stilarna innan du hinner byta flik.
Varför är det så mycket snabbare?#
Tre skäl:
1. Rust istället för JavaScript. CSS-parsern, klassdetektionen och kodgenereringen är alla nativ Rust. Det här är inte en "låt oss skriva om i Rust för nöjes skull"-situation — CSS-parsning är genuint CPU-bundet arbete där nativ kod har en massiv fördel gentemot V8.
2. Ingen PostCSS i den heta vägen. I v3 var Tailwind en PostCSS-plugin. Varje bygg innebar: parsa CSS till PostCSS AST, kör Tailwind-plugin, serialisera tillbaka till CSS-sträng, sedan körs andra PostCSS-plugins. I v4 har Tailwind sin egen CSS-parser som går direkt från källa till utdata. PostCSS stöds fortfarande för kompatibilitet, men den primära vägen skippar det helt.
3. Smartare inkrementell bearbetning. Den nya motorn cachar aggressivt. När du redigerar en enskild fil, skannar den bara den filen efter klassnamn och regenererar bara de CSS-regler som ändrades. V3-motorn var smartare på detta än folk ger den kredit för (JIT-läge var redan inkrementellt), men v4 tar det mycket längre med finkornig beroendesspårning.
Spelar hastighet verkligen roll?#
Ja, men inte av det skäl du förväntar dig. För de flesta projekt var v3:s bygghastighet "okej." Du väntade några hundra millisekunder i dev. Inte smärtsamt.
V4-hastigheten spelar roll eftersom den gör Tailwind osynligt i din verktygskedja. När byggen tar under 20ms slutar du tänka på Tailwind som ett byggsteg överhuvudtaget. Det blir som syntaxmarkering — alltid där, aldrig i vägen. Den psykologiska skillnaden är betydande under en hel dag av utveckling.
Nativ @layer-integration#
I v3 använde Tailwind sitt eget lagersystem med @layer base, @layer components och @layer utilities. Dessa såg ut som CSS-kaskadlager men var det inte — de var Tailwind-specifika direktiv som styrde var genererad CSS hamnade i utdatan.
I v4 använder Tailwind faktiska CSS-kaskadlager:
/* v4-utdata — förenklad */
@layer theme, base, components, utilities;
@layer base {
/* reset, preflight */
}
@layer components {
/* dina komponentklasser */
}
@layer utilities {
/* alla genererade verktygklasser */
}Detta är en betydande förändring eftersom CSS-kaskadlager har verkliga specificitetsimplikationer. En regel i ett lager med lägre prioritet förlorar alltid mot en regel i ett lager med högre prioritet, oavsett selektorspecificitet. Det betyder:
@layer components {
/* specificitet: 0-1-0 */
.card { padding: 1rem; }
}
@layer utilities {
/* specificitet: 0-1-0 — samma specificitet men vinner för att utilities-lagret kommer senare */
.p-4 { padding: 1rem; }
}Verktyg överskriver alltid komponenter. Komponenter överskriver alltid base. Det är så Tailwind fungerade konceptuellt i v3, men nu upprätthålls det av webbläsarens kaskadlagermekanism, inte genom manipulering av källordning.
Lägga till anpassade verktyg#
I v3 definierade du anpassade verktyg med ett plugin-API eller @layer utilities:
// v3 — plugin-metod
const plugin = require("tailwindcss/plugin");
module.exports = {
plugins: [
plugin(function ({ addUtilities }) {
addUtilities({
".text-balance": {
"text-wrap": "balance",
},
".text-pretty": {
"text-wrap": "pretty",
},
});
}),
],
};I v4 definieras anpassade verktyg med @utility-direktivet:
@import "tailwindcss";
@utility text-balance {
text-wrap: balance;
}
@utility text-pretty {
text-wrap: pretty;
}@utility-direktivet säger åt Tailwind "detta är en verktygsklass — placera den i utilities-lagret och tillåt att den används med varianter." Den sista delen är nyckeln. Ett verktyg definierat med @utility fungerar automatiskt med hover:, focus:, md: och alla andra varianter:
<p class="text-pretty md:text-balance">...</p>Anpassade varianter#
Du kan också definiera anpassade varianter med @variant:
@import "tailwindcss";
@variant hocus (&:hover, &:focus);
@variant theme-dark (.dark &);<button class="hocus:bg-brand-500 theme-dark:text-white">
Klicka på mig
</button>Detta ersätter v3:s addVariant plugin-API för de flesta användningsfall. Det är mindre kraftfullt (du kan inte göra programmatisk variantgenerering), men det täcker 90% av vad folk faktiskt gör.
Container Queries: Inbyggt, inget plugin#
Container queries var en av de mest efterfrågade funktionerna i v3. Du kunde få dem med @tailwindcss/container-queries-pluginet, men det var ett tillägg. I v4 är de inbyggda i ramverket.
Grundläggande användning#
Markera en container med @container och fråga dess storlek med @-prefixet:
<!-- markera förälder som en container -->
<div class="@container">
<!-- responsiv mot förälderns bredd, inte viewport -->
<div class="flex flex-col @md:flex-row @lg:grid @lg:grid-cols-3">
<div class="p-4">Kort 1</div>
<div class="p-4">Kort 2</div>
<div class="p-4">Kort 3</div>
</div>
</div>@md, @lg, etc.-varianterna fungerar som responsiva breakpoints men är relativa till närmaste @container-förfader istället för viewport. Breakpoint-värdena motsvarar Tailwinds standardbreakpoints:
| Variant | Min-width |
|---|---|
@sm | 24rem (384px) |
@md | 28rem (448px) |
@lg | 32rem (512px) |
@xl | 36rem (576px) |
@2xl | 42rem (672px) |
Namngivna containers#
Du kan namnge containers för att fråga specifika förfäder:
<div class="@container/sidebar">
<div class="@container/card">
<!-- frågar kort-containern -->
<div class="@md/card:text-lg">...</div>
<!-- frågar sidebar-containern -->
<div class="@lg/sidebar:hidden">...</div>
</div>
</div>Varför detta spelar roll#
Container queries förändrar hur du tänker om responsiv design. Istället för "vid denna viewport-bredd, visa tre kolumner," säger du "när den här komponentens container är tillräckligt bred, visa tre kolumner." Komponenter blir verkligen fristående. Du kan flytta en kortkomponent från en fullbredds-layout till en sidebar och den anpassas automatiskt. Ingen media query-gymnastik.
Jag har refaktorerat mina komponentbibliotek för att använda container queries som standard istället för viewport-breakpoints. Resultatet är komponenter som fungerar var du än placerar dem, utan att föräldern behöver veta något om komponentens responsiva beteende.
<!-- Denna komponent anpassas till VILKEN container den än placeras i -->
<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">Inläggsrubrik</h2>
<p class="mt-2 text-sm @md:text-base text-gray-600">
Inläggsutdrag här...
</p>
<div class="mt-4 hidden @md:flex gap-2">
<span class="text-xs bg-gray-100 px-2 py-1 rounded">Tagg</span>
</div>
</div>
</div>
</article>Nya varianter som faktiskt spelar roll#
v4 lägger till flera nya varianter som jag har sträckt mig efter konstant. De fyller verkliga luckor.
starting:-varianten#
Denna mappar till CSS @starting-style, som låter dig definiera initialtillståndet för ett element när det först visas. Detta är den saknade pusselbiten för att animera elementinträde utan JavaScript:
<dialog class="opacity-0 starting:opacity-0 open:opacity-100 transition-opacity duration-300">
<p>Denna dialog tonar in när den öppnas</p>
</dialog>starting:-varianten genererar CSS inuti ett @starting-style-block:
/* vad Tailwind genererar */
@starting-style {
dialog[open] {
opacity: 0;
}
}
dialog[open] {
opacity: 1;
transition: opacity 300ms;
}Detta är enormt för dialoger, popovers, dropdown-menyer — allt som behöver en inträdesanimation. Innan detta behövde du JavaScript för att lägga till en klass nästa frame, eller så använde du @keyframes. Nu är det en verktygsklass.
not-*-varianten#
Negation. Något vi har velat ha i evigheter:
<!-- alla barn utom det sista får en border -->
<div class="divide-y">
<div class="not-last:pb-4">Objekt 1</div>
<div class="not-last:pb-4">Objekt 2</div>
<div class="not-last:pb-4">Objekt 3</div>
</div>
<!-- styla allt som inte är disabled -->
<input class="not-disabled:hover:border-brand-500" />
<!-- negera data-attribut -->
<div class="not-data-active:opacity-50">...</div>nth-*-varianterna#
Direkt nth-child- och nth-of-type-åtkomst:
<ul>
<li class="nth-1:font-bold">Första objektet — fetstilt</li>
<li class="nth-even:bg-gray-50">Jämna rader — grå bakgrund</li>
<li class="nth-odd:bg-white">Udda rader — vit bakgrund</li>
<li class="nth-[3n+1]:text-brand-500">Var tredje+1 — varumärkesfärg</li>
</ul>Hakparentessyntaxen (nth-[3n+1]) stöder alla giltiga nth-child-uttryck. Detta ersätter en hel del anpassad CSS jag brukade skriva för tabellrandning och rutnätsmönster.
in-*-varianten (förälderens tillstånd)#
Detta är inversen av group-*. Istället för "när min förälder (group) hovras, styla mig," är det "när jag är inuti en förälder som matchar detta tillstånd, styla mig":
<div class="in-data-active:bg-brand-50">
Denna får en bakgrund när någon förfader har data-active
</div>**:-varianten för djupa universella val#
Styla alla ättlingar, inte bara direkta barn. Detta är kontrollerad kraft — använd det sparsamt, men det är ovärderligt för prosainnehåll och CMS-utdata:
<!-- alla paragrafer inuti denna div, på valfritt djup -->
<div class="**:data-highlight:bg-yellow-100">
<section>
<p data-highlight>Denna får markering</p>
<div>
<p data-highlight>Så gör denna, nästlad djupare</p>
</div>
</section>
</div>Breaking Changes: Vad som faktiskt gick sönder#
Låt mig vara rättfram. Om du har ett stort v3-projekt är migreringen inte trivial. Här är vad som gick sönder i mina projekt:
1. Konfigurationsformat#
Din tailwind.config.js fungerar inte direkt. Du behöver antingen:
- Konvertera den till
@themeCSS (rekommenderat för ny arkitektur) - Använda kompatibilitetslagret
@config-direktivet (snabb migreringsväg)
/* snabb migrering — behåll din gamla konfig */
@import "tailwindcss";
@config "../../tailwind.config.js";Denna @config-brygga fungerar, men den är uttryckligen ett migreringsverktyg. Rekommendationen är att flytta till @theme med tiden.
2. Borttagna föråldrade verktyg#
Vissa verktyg som var föråldrade i v3 är borta:
/* BORTTAGNA i v4 */
bg-opacity-* → använd bg-black/50 (snedstreck-opacitetssyntax)
text-opacity-* → använd text-black/50
border-opacity-* → använd border-black/50
flex-shrink-* → använd shrink-*
flex-grow-* → använd grow-*
overflow-ellipsis → använd text-ellipsis
decoration-slice → använd box-decoration-slice
decoration-clone → använd box-decoration-clone
Om du redan använde den moderna syntaxen i v3 (snedstreck-opacitet, shrink-*), är du klar. Om inte, är dessa enkla sök-och-ersätt-ändringar.
3. Ändringar i standardfärgpaletten#
Standardfärgpaletten skiftade något. Om du beror på exakta färgvärden från v3 (inte efter namn utan efter det faktiska hex-värdet), kan du märka visuella skillnader. De namngivna färgerna (blue-500, gray-200) finns fortfarande men vissa hex-värden ändrades.
4. Innehållsdetektering#
v3 krävde explicit content-konfiguration:
// v3
module.exports = {
content: [
"./src/**/*.{js,ts,jsx,tsx}",
"./components/**/*.{js,ts,jsx,tsx}",
],
};v4 använder automatisk innehållsdetektering. Den skannar din projektrot och hittar mallfiler automatiskt. Detta "bara fungerar" oftast, men om du har en ovanlig projektstruktur (monorepo med paket utanför projektroten, mallfiler på oväntade platser), kan du behöva konfigurera källsökvägar explicit:
@import "tailwindcss";
@source "../shared-components/**/*.tsx";
@source "../design-system/src/**/*.tsx";5. Ändringar i plugin-API#
Om du skrev anpassade plugins ändrades API:t. Funktionerna addUtilities, addComponents, addBase och addVariant fungerar fortfarande genom kompatibilitetslagret, men det idiomatiska v4-tillvägagångssättet är CSS-nativt:
// v3-plugin
plugin(function ({ addUtilities, theme }) {
addUtilities({
".scrollbar-hide": {
"-ms-overflow-style": "none",
"scrollbar-width": "none",
"&::-webkit-scrollbar": {
display: "none",
},
},
});
});/* v4 — bara CSS */
@utility scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}De flesta förstapartsplugins (@tailwindcss/typography, @tailwindcss/forms, @tailwindcss/aspect-ratio) har v4-kompatibla versioner. Tredjepartsplugins är osäkra — kontrollera deras repo innan du migrerar.
6. JIT är det enda läget#
I v3 kunde du välja bort JIT-läge (även om nästan ingen gjorde det). I v4 finns det inget icke-JIT-läge. Allt genereras på begäran, alltid. Om du hade någon anledning att använda den gamla AOT-motorn (ahead-of-time) är den vägen borta.
7. Vissa ändringar i variantsyntax#
Några varianter bytte namn eller ändrade beteende:
<!-- v3 -->
<div class="[&>*]:p-4">...</div>
<!-- v4 — >*-delen använder nu inset-variantsyntaxen -->
<div class="*:p-4">...</div>Den godtyckliga variantsyntaxen [&...] fungerar fortfarande, men v4 ger namngivna alternativ för vanliga mönster.
Migreringsguide: Den verkliga processen#
Så här migrerade jag faktiskt, inte den lyckliga vägen från dokumentationen utan hur processen verkligen såg ut.
Steg 1: Kör den officiella codemod#
Tailwind tillhandahåller en codemod som hanterar de flesta mekaniska ändringar:
npx @tailwindcss/upgradeDenna gör mycket automatiskt:
- Konverterar
@tailwind-direktiv till@import "tailwindcss" - Byter namn på föråldrade verktygsklasser
- Uppdaterar variantsyntax
- Konverterar opacitetsverktyg till snedstreckssyntax (
bg-opacity-50tillbg-black/50) - Skapar ett grundläggande
@theme-block från din konfig
Vad codemod hanterar bra#
- Namnbyten av verktygsklasser (nästan perfekt)
- Syntaxändringar för direktiv
- Enkla temavärden (färger, avstånd, typsnitt)
- Migrering av opacitetssyntax
Vad codemod INTE hanterar#
- Komplexa pluginkonverteringar
- Dynamiska konfigvärden (
theme()-anrop i JavaScript) - Villkorlig temakonfiguration (t.ex. temavärden baserade på miljö)
- Migrering av anpassat plugin-API
- Specialfall med godtyckliga värden där den nya parsern tolkar annorlunda
- Klassnamn konstruerade dynamiskt i JavaScript (template literals, strängkonkatenering)
Steg 2: Fixa PostCSS-konfiguration#
För de flesta uppsättningar uppdaterar du din PostCSS-konfig:
// postcss.config.js — v4
module.exports = {
plugins: {
"@tailwindcss/postcss": {},
},
};Observera: pluginnamnet ändrades från tailwindcss till @tailwindcss/postcss. Om du använder Vite kan du hoppa över PostCSS helt och använda Vite-pluginet:
// vite.config.ts
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
plugins: [tailwindcss()],
});Steg 3: Konvertera temakonfiguration#
Detta är den manuella delen. Ta dina tailwind.config.js-temavärden och konvertera dem till @theme:
// v3-konfig — före
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 — efter */
@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; }
}Notera att keyframes flyttas ut ur @theme och blir vanliga CSS @keyframes. Animationsnamnet i @theme refererar bara till dem. Det är renare — keyframes är CSS, de borde skrivas som CSS.
Steg 4: Visuell regressionstestning#
Detta är icke-förhandlingsbart. Efter migrering öppnade jag varje sida i min app och kontrollerade den visuellt. Jag körde också mina Playwright-skärmdumpstester (om du har dem). Codemod är bra men inte perfekt. Saker jag upptäckte i visuell granskning:
- Några ställen där migrering av opacitetssyntax producerade lite annorlunda resultat
- Utdata från anpassade plugins som inte överfördes
- Z-index-stackningsändringar på grund av lagerordning
- Vissa
!important-överskrivningar som betedde sig annorlunda med kaskadlager
Steg 5: Uppdatera tredjepartsberoenden#
Kontrollera varje Tailwind-relaterat paket:
{
"@tailwindcss/typography": "^1.0.0",
"@tailwindcss/forms": "^1.0.0",
"@tailwindcss/container-queries": "TA BORT — inbyggt nu",
"tailwindcss-animate": "kontrollera v4-stöd",
"prettier-plugin-tailwindcss": "uppdatera till senaste"
}@tailwindcss/container-queries-pluginet behövs inte längre — container queries är inbyggda. Andra plugins behöver sina v4-kompatibla versioner.
Arbeta med Next.js#
Eftersom jag använder Next.js för de flesta projekt, här är den specifika uppsättningen.
PostCSS-metod (rekommenderad för Next.js)#
Next.js använder PostCSS under huven, så PostCSS-pluginet är det naturliga valet:
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;
}Det är hela uppsättningen. Ingen tailwind.config.js, ingen autoprefixer (v4 hanterar vendor-prefix internt).
CSS-importordning#
En sak som snubblade mig: CSS-importordning spelar mer roll i v4 på grund av kaskadlager. Din @import "tailwindcss" bör komma före dina anpassade stilar:
/* korrekt ordning */
@import "tailwindcss";
@import "./theme.css";
@import "./custom-utilities.css";
/* dina inline @theme, @utility, etc. */Om du importerar anpassad CSS före Tailwind kan dina stilar hamna i ett kaskadlager med lägre prioritet och överskridas oväntat.
Mörkt läge#
Mörkt läge fungerar likadant konceptuellt men konfigurationen flyttade till CSS:
@import "tailwindcss";
/* Använd klassbaserat mörkt läge (standard är mediabaserat) */
@variant dark (&:where(.dark, .dark *));Detta ersätter v3-konfigen:
// v3
module.exports = {
darkMode: "class",
};@variant-metoden är mer flexibel. Du kan definiera mörkt läge hur du vill — klassbaserat, data-attributbaserat eller media query-baserat:
/* data-attributmetod */
@variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));
/* media query — detta är standard, så du behöver inte deklarera det */
@variant dark (@media (prefers-color-scheme: dark));Turbopack-kompatibilitet#
Om du använder Next.js med Turbopack (som nu är standardutvecklingsbundlern), fungerar v4 utmärkt. Rust-motorn samspelar väl med Turbopacks egen Rust-baserade arkitektur. Jag mätte dev-starttider:
| Uppsättning | v3 + Webpack | v3 + Turbopack | v4 + Turbopack |
|---|---|---|---|
| Kallstart | 4.8s | 2.1s | 1.3s |
| HMR (CSS-ändring) | 450ms | 180ms | 40ms |
40ms HMR för CSS-ändringar är knappt märkbart. Det känns omedelbart.
Fördjupning i prestanda: Bortom bygghastighet#
Oxide-motorns fördelar sträcker sig bortom ren bygghastighet.
Minnesanvändning#
v4 använder betydligt mindre minne. På mitt projekt med 847 komponenter:
| Mätvärde | v3 | v4 |
|---|---|---|
| Toppminne (bygg) | 380MB | 45MB |
| Stabilt tillstånd (dev) | 210MB | 28MB |
Detta spelar roll för CI/CD-pipelines där minnet är begränsat, och för utvecklingsmaskiner som kör tio processer samtidigt.
CSS-utdatastorlek#
v4 genererar något mindre CSS-utdata eftersom den nya motorn är bättre på deduplicering och eliminering av död kod:
v3-utdata: 34.2 KB (gzippad)
v4-utdata: 29.8 KB (gzippad)
En 13% minskning utan att ändra någon kod. Inte omvälvande, men gratis prestanda.
Tree Shaking av temavärden#
I v4, om du definierar ett temavärde men aldrig använder det i dina mallar, emitteras fortfarande motsvarande CSS custom property (den är i @theme, som mappar till :root-variabler). Dock genereras inte verktygsklasserna för oanvända värden. Detta är samma sak som v3:s JIT-beteende men värt att notera: dina CSS custom properties är alltid tillgängliga, även för värden utan verktygsanvändning.
Om du vill förhindra att vissa temavärden genererar CSS custom properties kan du använda @theme inline:
@theme inline {
/* Dessa värden genererar verktyg men INTE CSS custom properties */
--color-internal-debug: #ff00ff;
--spacing-magic-number: 3.7rem;
}Detta är användbart för interna designtokens som du inte vill exponera som CSS-variabler.
Avancerat: Komponera teman för flera varumärken#
Ett mönster som v4 gör betydligt enklare är fleruvarumärkestematisering. Eftersom temavärden är CSS custom properties kan du byta dem vid körtid:
@import "tailwindcss";
@theme {
--color-brand: var(--brand-primary, #3b82f6);
--color-brand-light: var(--brand-light, #60a5fa);
--color-brand-dark: var(--brand-dark, #1d4ed8);
}
/* Varumärkesöverskrivningar */
.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">
<!-- alla bg-brand, text-brand, etc. använder Acme-färger -->
<div class="bg-brand text-white">Acme Corp</div>
</body>I v3 krävde detta ett anpassat plugin eller komplex CSS-variabeluppsättning utanför Tailwind. I v4 är det naturligt — temat är CSS-variabler, och CSS-variabler kaskaderar. Det här är den typen av saker som gör att CSS-first-metoden känns rätt.
Vad jag saknar från v3#
Låt mig vara balanserad. Det finns saker som v3 gjorde som jag genuint saknar i v4:
1. JavaScript-konfig för programmatiska teman. Jag hade ett projekt där vi genererade färgskalor från en enda varumärkesfärg med en JavaScript-funktion i konfigurationen. I v4 kan du inte göra det i @theme — du skulle behöva ett byggsteg som genererar CSS-filen, eller så beräknar du färgerna en gång och klistrar in dem. @config-kompatibilitetslagret hjälper, men det är inte den långsiktiga lösningen.
2. IntelliSense var bättre vid lansering. V3 VS Code-tillägget hade år av polering. V4 IntelliSense fungerar men hade vissa luckor tidigt — anpassade @theme-värden autokompleterades ibland inte, och @utility-definitioner fångades inte alltid upp. Detta har förbättrats avsevärt med senaste uppdateringar, men det är värt att notera.
3. Ekosystemmognad. Ekosystemet runt v3 var enormt. Headless UI, Radix, shadcn/ui, Flowbite, DaisyUI — allt var testat mot v3. V4-stöd rullar ut men är inte universellt. Jag var tvungen att skicka en PR till ett komponentbibliotek för att fixa v4-kompatibilitet.
Bör du migrera?#
Här är mitt beslutsramverk efter att ha levt med v4 i flera veckor:
Migrera nu om:#
- Du startar ett nytt projekt (självklart val — börja med v4)
- Ditt projekt har minimalt med anpassade plugins
- Du vill ha prestandafördelarna för stora projekt
- Du redan använder moderna Tailwind-mönster (snedstreck-opacitet,
shrink-*, etc.) - Du behöver container queries och inte vill lägga till ett plugin
Vänta om:#
- Du beror starkt på tredjepartstailwind-plugins som inte stöder v4 ännu
- Du har komplex programmatisk temakonfiguration
- Ditt projekt är stabilt och inte aktivt utvecklat (varför röra det?)
- Du är mitt i en funktionssprint (migrera mellan sprintar, inte under)
Migrera inte om:#
- Du är på v2 eller tidigare (uppgradera till v3 först, stabilisera, överväg sedan v4)
- Ditt projekt slutar om några månader (inte värt oron)
Min ärliga bedömning#
För nya projekt är v4 det självklara valet. CSS-first-konfigurationen är renare, motorn är dramatiskt snabbare, och de nya funktionerna (container queries, @starting-style, nya varianter) är genuint användbara.
För befintliga projekt rekommenderar jag en stegvis metod:
- Nu: Starta alla nya projekt på v4
- Snart: Experimentera genom att konvertera ett litet internt projekt till v4
- När du är redo: Migrera produktionsprojekt under en lugn sprint, med visuell regressionstestning
Migreringen är inte smärtsam om du förbereder dig. Codemod hanterar 80% av arbetet. De återstående 20% är manuella men okomplicerade. Budgetera en dag för ett medelstort projekt, två till tre dagar för ett stort.
Tailwind v4 är vad Tailwind borde ha varit hela tiden. JavaScript-konfigurationen var alltid en eftergift åt verktygslandskapet i sin tid. CSS-first-konfiguration, nativa kaskadlager, en Rust-motor — det här är inte trender, det är ramverket som kommer ikapp plattformen. Webbplattformen blev bättre, och Tailwind v4 lutar sig in i det istället för att kämpa emot det.
Flytten till att skriva dina designtokens i CSS, komponera dem med CSS-funktioner och låta webbläsarens egen kaskad hantera specificitet — det är rätt riktning. Det tog fyra stora versioner att komma hit, men resultatet är den mest sammanhängande versionen av Tailwind hittills.
Starta ditt nästa projekt med det. Du kommer inte ångra dig.