Tailwind CSS v4: Co się naprawdę zmieniło i czy warto migrować
Konfiguracja CSS-first, integracja z @layer, wbudowane container queries, wydajność nowego silnika, breaking changes i moje szczere doświadczenie z migracji z v3 na v4.
Używam Tailwind CSS od v1.x, jeszcze wtedy, gdy połowa społeczności uważała go za ohydę, a druga połowa nie mogła przestać z nim shippować. Każda wersja major była znaczącym skokiem, ale v4 jest inna. To nie jest po prostu release z nowymi funkcjami. To architektoniczna przebudowa od podstaw, która zmienia fundamentalny kontrakt między tobą a frameworkiem.
Po migracji dwóch projektów produkcyjnych z v3 na v4 i rozpoczęciu trzech nowych projektów na v4 od zera, mam jasny obraz tego, co jest naprawdę lepsze, co jest surowe i czy powinieneś migrować dziś. Żadnego hype'u, żadnego oburzenia — tylko to, co zaobserwowałem.
Ogólny obraz: Czym v4 faktycznie jest#
Tailwind CSS v4 to trzy rzeczy naraz:
- Nowy silnik — przepisany z JavaScript na Rust (silnik Oxide), dramatycznie przyspieszający buildy
- Nowy paradygmat konfiguracji — konfiguracja CSS-first zastępuje
tailwind.config.jsjako domyślną - Ściślejsza integracja z platformą CSS — natywne
@layer, container queries,@starting-stylei cascade layers to obywatele pierwszej klasy
Nagłówek, który zobaczysz wszędzie, to „10x szybciej." To prawdziwe, ale nie oddaje faktycznej zmiany. Model mentalny konfigurowania i rozszerzania Tailwind fundamentalnie się zmienił. Teraz pracujesz z CSS, a nie z obiektem konfiguracyjnym JavaScript, który generuje CSS.
Oto jak wygląda minimalny setup Tailwind v4:
/* app.css — to cały setup */
@import "tailwindcss";To wszystko. Żadnego pliku konfiguracyjnego. Żadnej konfiguracji pluginu PostCSS (dla większości setupów). Żadnych dyrektyw @tailwind base; @tailwind components; @tailwind utilities;. Jeden import i działasz.
Porównaj to z 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: {},
},
};Trzy pliki sprowadzone do jednej linii. To nie tylko mniej boilerplate'u — to mniej powierzchni do błędnej konfiguracji. W v4 wykrywanie treści jest automatyczne. Skanuje pliki twojego projektu bez potrzeby podawania wzorców glob.
Konfiguracja CSS-first z @theme#
To największa zmiana koncepcyjna. W v3 customizowałeś Tailwind przez obiekt konfiguracyjny JavaScript:
// 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",
},
},
},
};W v4 wszystko to żyje w CSS przy użyciu dyrektywy @theme:
@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;
}Na początku opierałem się temu. Lubiłem mieć pojedynczy obiekt JavaScript, gdzie mogłem widzieć cały swój design system. Ale po tygodniu z podejściem CSS zmieniłem zdanie z trzech powodów:
1. Natywne CSS custom properties są eksponowane automatycznie. Każda wartość zdefiniowana w @theme staje się CSS custom property na :root. Oznacza to, że wartości twojego motywu są dostępne w zwykłym CSS, w CSS Modules, w tagach <style>, wszędzie gdzie działa CSS:
/* dostajesz to za darmo */
:root {
--color-brand-50: #eff6ff;
--color-brand-500: #3b82f6;
--color-brand-900: #1e3a5f;
}/* używaj ich gdziekolwiek — Tailwind nie jest potrzebny */
.custom-element {
border: 2px solid var(--color-brand-500);
}2. Możesz używać funkcji CSS wewnątrz @theme. Media queries, light-dark(), calc() — prawdziwy CSS tu działa, bo to jest prawdziwy CSS:
@theme {
--color-surface: light-dark(#ffffff, #0a0a0a);
--color-text: light-dark(#0a0a0a, #fafafa);
--spacing-container: calc(100vw - 2rem);
}3. Kolokacja z resztą twojego CSS. Twój motyw, niestandardowe klasy utility i style bazowe — wszystko żyje w tym samym języku, w tym samym pliku jeśli chcesz. Żadnego przełączania kontekstu między „światem CSS" a „światem konfiguracji JavaScript."
Nadpisywanie vs rozszerzanie domyślnego motywu#
W v3 miałeś theme (zastąp) vs theme.extend (scal). W v4 model mentalny jest inny:
@import "tailwindcss";
/* To ROZSZERZA domyślny motyw — dodaje kolory brand obok istniejących */
@theme {
--color-brand-500: #3b82f6;
}Jeśli chcesz całkowicie zastąpić namespace (jak usunięcie wszystkich domyślnych kolorów), używasz @theme z resetem wildcard --color-*:
@import "tailwindcss";
@theme {
/* Najpierw wyczyść wszystkie domyślne kolory */
--color-*: initial;
/* Teraz zdefiniuj tylko swoje kolory */
--color-white: #ffffff;
--color-black: #000000;
--color-brand-50: #eff6ff;
--color-brand-500: #3b82f6;
--color-brand-900: #1e3a5f;
}Ten wzorzec resetu wildcard jest elegancki. Wybierasz dokładnie, które części domyślnego motywu zachować, a które zastąpić. Chcesz wszystkie domyślne odstępy, ale niestandardowe kolory? Zresetuj --color-*: initial; i zostaw odstępy w spokoju.
Wiele plików motywu#
Dla większych projektów możesz podzielić motyw na pliki:
/* 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";To jest znacznie czystsze niż wzorzec v3 z gigantycznym tailwind.config.js lub próbami dzielenia go za pomocą require().
Silnik Oxide: Naprawdę 10x szybciej#
Silnik Tailwind v4 to kompletna przebudowa w Rust. Nazywają go Oxide. Byłem sceptyczny wobec twierdzenia „10x szybciej" — liczby marketingowe rzadko przeżywają kontakt z prawdziwymi projektami. Więc zbenchmarkowałem to.
Mój projekt testowy: Aplikacja Next.js z 847 komponentami, 142 stronami, około 23 000 użyciami klas Tailwind.
| Metryka | v3 (Node) | v4 (Oxide) | Poprawa |
|---|---|---|---|
| Początkowy build | 4 280ms | 387ms | 11x |
| Inkrementalny (edycja 1 pliku) | 340ms | 18ms | 19x |
| Pełny rebuild (clean) | 5 100ms | 510ms | 10x |
| Start serwera dev | 3 200ms | 290ms | 11x |
Twierdzenie „10x" jest konserwatywne dla mojego projektu. Buildy inkrementalne to miejsce, gdzie naprawdę lśni — 18ms oznacza, że jest zasadniczo natychmiastowy. Zapisujesz plik i przeglądarka ma nowe style, zanim zdążysz przełączyć kartę.
Dlaczego jest taki szybki?#
Trzy powody:
1. Rust zamiast JavaScript. Rdzeń parsera CSS, wykrywanie klas i generowanie kodu to natywny Rust. To nie jest sytuacja „przepiszmy w Rust dla zabawy" — parsowanie CSS to naprawdę praca obciążająca CPU, gdzie natywny kod ma ogromną przewagę nad V8.
2. Brak PostCSS na gorącej ścieżce. W v3 Tailwind był pluginem PostCSS. Każdy build oznaczał: parsowanie CSS do AST PostCSS, uruchomienie pluginu Tailwind, serializację z powrotem do ciągu CSS, a potem działanie innych pluginów PostCSS. W v4 Tailwind ma własny parser CSS, który idzie bezpośrednio od źródła do wyjścia. PostCSS jest nadal obsługiwany dla kompatybilności, ale główna ścieżka go całkowicie pomija.
3. Sprytniejsze przetwarzanie inkrementalne. Nowy silnik agresywnie cachuje. Gdy edytujesz pojedynczy plik, ponownie skanuje tylko ten plik w poszukiwaniu nazw klas i regeneruje tylko zmienione reguły CSS. Silnik v3 był pod tym względem sprytniejszy, niż ludziom się wydaje (tryb JIT już był inkrementalny), ale v4 idzie znacznie dalej dzięki precyzyjnemu śledzeniu zależności.
Czy szybkość naprawdę ma znaczenie?#
Tak, ale nie z powodu, jakiego byś oczekiwał. Dla większości projektów szybkość buildów w v3 była „wystarczająca." Czekałeś kilkaset milisekund w trybie dev. Nie bolesne.
Szybkość v4 ma znaczenie, bo sprawia, że Tailwind staje się niewidzialny w twoim toolchainie. Gdy buildy trwają poniżej 20ms, przestajesz myśleć o Tailwind jako o kroku budowania w ogóle. Staje się jak podświetlanie składni — zawsze obecne, nigdy na przeszkodzie. Ta psychologiczna różnica jest znacząca w ciągu pełnego dnia programowania.
Natywna integracja @layer#
W v3 Tailwind używał własnego systemu warstw z @layer base, @layer components i @layer utilities. Wyglądały jak cascade layers CSS, ale nimi nie były — były specyficznymi dla Tailwind dyrektywami, które kontrolowały, gdzie wygenerowany CSS pojawiał się w wyjściu.
W v4 Tailwind używa faktycznych cascade layers CSS:
/* wyjście v4 — uproszczone */
@layer theme, base, components, utilities;
@layer base {
/* reset, preflight */
}
@layer components {
/* twoje klasy komponentów */
}
@layer utilities {
/* wszystkie wygenerowane klasy utility */
}To znacząca zmiana, bo CSS cascade layers mają realne implikacje specyficzności. Reguła w warstwie o niższym priorytecie zawsze przegrywa z regułą w warstwie o wyższym priorytecie, niezależnie od specyficzności selektora. Oznacza to:
@layer components {
/* specyficzność: 0-1-0 */
.card { padding: 1rem; }
}
@layer utilities {
/* specyficzność: 0-1-0 — taka sama, ale wygrywa, bo warstwa utilities jest późniejsza */
.p-4 { padding: 1rem; }
}Klasy utility zawsze nadpisują komponenty. Komponenty zawsze nadpisują bazę. Tak Tailwind działał koncepcyjnie w v3, ale teraz jest to wymuszane przez mechanizm cascade layers przeglądarki, a nie przez manipulację kolejnością źródła.
Dodawanie niestandardowych klas utility#
W v3 definiowałeś niestandardowe klasy utility za pomocą API pluginów lub @layer utilities:
// v3 — podejście z pluginem
const plugin = require("tailwindcss/plugin");
module.exports = {
plugins: [
plugin(function ({ addUtilities }) {
addUtilities({
".text-balance": {
"text-wrap": "balance",
},
".text-pretty": {
"text-wrap": "pretty",
},
});
}),
],
};W v4 niestandardowe klasy utility definiuje się dyrektywą @utility:
@import "tailwindcss";
@utility text-balance {
text-wrap: balance;
}
@utility text-pretty {
text-wrap: pretty;
}Dyrektywa @utility mówi Tailwindowi „to jest klasa utility — umieść ją w warstwie utilities i pozwól na użycie z wariantami." Ta ostatnia część jest kluczowa. Klasa utility zdefiniowana z @utility automatycznie działa z hover:, focus:, md: i każdym innym wariantem:
<p class="text-pretty md:text-balance">...</p>Niestandardowe warianty#
Możesz też definiować niestandardowe warianty za pomocą @variant:
@import "tailwindcss";
@variant hocus (&:hover, &:focus);
@variant theme-dark (.dark &);<button class="hocus:bg-brand-500 theme-dark:text-white">
Kliknij mnie
</button>Zastępuje to API addVariant z pluginów v3 w większości przypadków użycia. Jest mniej potężne (nie można programistycznie generować wariantów), ale pokrywa 90% tego, co ludzie faktycznie robią.
Container queries: Wbudowane, bez pluginu#
Container queries były jedną z najbardziej pożądanych funkcji w v3. Można było je uzyskać za pomocą pluginu @tailwindcss/container-queries, ale to był dodatek. W v4 są wbudowane w framework.
Podstawowe użycie#
Oznacz kontener za pomocą @container i odpytuj jego rozmiar prefiksem @:
<!-- oznacz rodzica jako kontener -->
<div class="@container">
<!-- responsywne względem szerokości rodzica, nie viewportu -->
<div class="flex flex-col @md:flex-row @lg:grid @lg:grid-cols-3">
<div class="p-4">Karta 1</div>
<div class="p-4">Karta 2</div>
<div class="p-4">Karta 3</div>
</div>
</div>Warianty @md, @lg itd. działają jak responsywne breakpointy, ale odnoszą się do najbliższego przodka @container zamiast viewportu. Wartości breakpointów odpowiadają domyślnym breakpointom Tailwind:
| Wariant | Minimalna szerokość |
|---|---|
@sm | 24rem (384px) |
@md | 28rem (448px) |
@lg | 32rem (512px) |
@xl | 36rem (576px) |
@2xl | 42rem (672px) |
Nazwane kontenery#
Możesz nazywać kontenery, aby odpytywać konkretnych przodków:
<div class="@container/sidebar">
<div class="@container/card">
<!-- odpytuje kontener card -->
<div class="@md/card:text-lg">...</div>
<!-- odpytuje kontener sidebar -->
<div class="@lg/sidebar:hidden">...</div>
</div>
</div>Dlaczego to ma znaczenie#
Container queries zmieniają sposób myślenia o responsywnym designie. Zamiast „przy tej szerokości viewportu pokaż trzy kolumny" mówisz „gdy kontener tego komponentu jest wystarczająco szeroki, pokaż trzy kolumny." Komponenty stają się naprawdę samodzielne. Możesz przenieść komponent karty z pełnowymiarowego layoutu do paska bocznego i automatycznie się dostosuje. Żadnej gimnastyki z media queries.
Refaktoryzuję swoje biblioteki komponentów, aby domyślnie używały container queries zamiast breakpointów viewportu. Rezultatem są komponenty, które działają wszędzie, gdzie je umieścisz, bez konieczności wiedzy rodzica o responsywnym zachowaniu komponentu.
<!-- Ten komponent dostosowuje się do KAŻDEGO kontenera, w którym jest umieszczony -->
<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">Tytuł wpisu</h2>
<p class="mt-2 text-sm @md:text-base text-gray-600">
Fragment wpisu umieszczony tutaj...
</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>Nowe warianty, które naprawdę mają znaczenie#
v4 dodaje kilka nowych wariantów, po które ciągle sięgam. Wypełniają realne luki.
Wariant starting:#
Mapuje się na CSS @starting-style, który pozwala zdefiniować początkowy stan elementu, gdy się po raz pierwszy pojawia. To brakujący element do animowania wejścia elementu bez JavaScript:
<dialog class="opacity-0 starting:opacity-0 open:opacity-100 transition-opacity duration-300">
<p>Ten dialog pojawia się z efektem fade-in po otwarciu</p>
</dialog>Wariant starting: generuje CSS wewnątrz bloku @starting-style:
/* co Tailwind generuje */
@starting-style {
dialog[open] {
opacity: 0;
}
}
dialog[open] {
opacity: 1;
transition: opacity 300ms;
}To ogromna zmiana dla dialogów, popoverów, menu rozwijanych — czegokolwiek, co potrzebuje animacji wejścia. Wcześniej trzeba było użyć JavaScript, aby dodać klasę na następnej klatce, albo używać @keyframes. Teraz to klasa utility.
Wariant not-*#
Negacja. Coś, czego chcieliśmy od zawsze:
<!-- każde dziecko oprócz ostatniego dostaje obramowanie -->
<div class="divide-y">
<div class="not-last:pb-4">Element 1</div>
<div class="not-last:pb-4">Element 2</div>
<div class="not-last:pb-4">Element 3</div>
</div>
<!-- styluj wszystko, co nie jest wyłączone -->
<input class="not-disabled:hover:border-brand-500" />
<!-- neguj atrybuty data -->
<div class="not-data-active:opacity-50">...</div>Warianty nth-*#
Bezpośredni dostęp do nth-child i nth-of-type:
<ul>
<li class="nth-1:font-bold">Pierwszy element — pogrubiony</li>
<li class="nth-even:bg-gray-50">Parzyste wiersze — szare tło</li>
<li class="nth-odd:bg-white">Nieparzyste wiersze — białe tło</li>
<li class="nth-[3n+1]:text-brand-500">Co trzeci+1 — kolor marki</li>
</ul>Składnia z nawiasami (nth-[3n+1]) obsługuje dowolne prawidłowe wyrażenie nth-child. Zastępuje to sporo niestandardowego CSS, który kiedyś pisałem do pasków tabel i wzorców siatki.
Wariant in-* (stan rodzica)#
To odwrotność group-*. Zamiast „gdy mój rodzic (group) ma hover, styluj mnie" to „gdy jestem wewnątrz rodzica pasującego do tego stanu, styluj mnie":
<div class="in-data-active:bg-brand-50">
Ten element dostaje tło, gdy jakikolwiek przodek ma data-active
</div>Wariant głęboki **:#
Styluj wszystkich potomków, nie tylko bezpośrednie dzieci. To kontrolowana siła — używaj oszczędnie, ale jest nieoceniona dla treści prozy i wyjścia CMS:
<!-- wszystkie paragrafy wewnątrz tego div, na dowolnej głębokości -->
<div class="**:data-highlight:bg-yellow-100">
<section>
<p data-highlight>Ten jest podświetlony</p>
<div>
<p data-highlight>Ten też, zagnieżdżony głębiej</p>
</div>
</section>
</div>Breaking changes: Co się naprawdę zepsuło#
Powiem wprost. Jeśli masz duży projekt v3, migracja nie jest trywialna. Oto co się zepsuło w moich projektach:
1. Format konfiguracji#
Twój tailwind.config.js nie działa od razu. Musisz albo:
- Przekonwertować go na CSS
@theme(zalecane dla nowej architektury) - Użyć warstwy kompatybilności z dyrektywą
@config(szybka ścieżka migracji)
/* szybka migracja — zachowaj starą konfigurację */
@import "tailwindcss";
@config "../../tailwind.config.js";Ten mostek @config działa, ale jest wyraźnie narzędziem migracyjnym. Zalecenie to stopniowe przechodzenie na @theme.
2. Usunięte przestarzałe klasy utility#
Niektóre klasy utility, które były przestarzałe w v3, zniknęły:
/* USUNIĘTE w v4 */
bg-opacity-* → użyj bg-black/50 (składnia slash opacity)
text-opacity-* → użyj text-black/50
border-opacity-* → użyj border-black/50
flex-shrink-* → użyj shrink-*
flex-grow-* → użyj grow-*
overflow-ellipsis → użyj text-ellipsis
decoration-slice → użyj box-decoration-slice
decoration-clone → użyj box-decoration-clone
Jeśli już używałeś nowoczesnej składni w v3 (slash opacity, shrink-*), nic ci nie grozi. Jeśli nie, to proste zmiany typu znajdź-i-zamień.
3. Zmiany domyślnej palety kolorów#
Domyślna paleta kolorów nieznacznie się przesunęła. Jeśli polegasz na dokładnych wartościach kolorów z v3 (nie po nazwie, ale po faktycznej wartości hex), możesz zauważyć różnice wizualne. Kolory nazwane (blue-500, gray-200) nadal istnieją, ale niektóre wartości hex się zmieniły.
4. Wykrywanie treści#
v3 wymagał jawnej konfiguracji content:
// v3
module.exports = {
content: [
"./src/**/*.{js,ts,jsx,tsx}",
"./components/**/*.{js,ts,jsx,tsx}",
],
};v4 używa automatycznego wykrywania treści. Skanuje korzeń twojego projektu i automatycznie znajduje pliki szablonów. W większości „po prostu działa," ale jeśli masz nietypową strukturę projektu (monorepo z pakietami poza rootem projektu, pliki szablonów w nieoczekiwanych lokalizacjach), może być konieczne jawne skonfigurowanie ścieżek źródeł:
@import "tailwindcss";
@source "../shared-components/**/*.tsx";
@source "../design-system/src/**/*.tsx";5. Zmiany API pluginów#
Jeśli pisałeś niestandardowe pluginy, API się zmieniło. Funkcje addUtilities, addComponents, addBase i addVariant nadal działają przez warstwę kompatybilności, ale idiomatyczne podejście v4 to natywny CSS:
// plugin v3
plugin(function ({ addUtilities, theme }) {
addUtilities({
".scrollbar-hide": {
"-ms-overflow-style": "none",
"scrollbar-width": "none",
"&::-webkit-scrollbar": {
display: "none",
},
},
});
});/* v4 — po prostu CSS */
@utility scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}Większość pluginów pierwszej strony (@tailwindcss/typography, @tailwindcss/forms, @tailwindcss/aspect-ratio) ma wersje kompatybilne z v4. Pluginy firm trzecich — to zależy. Sprawdź ich repozytorium przed migracją.
6. JIT jest jedynym trybem#
W v3 można było zrezygnować z trybu JIT (choć prawie nikt tego nie robił). W v4 nie ma trybu innego niż JIT. Wszystko jest generowane na żądanie, zawsze. Jeśli z jakiegoś powodu używałeś starego silnika AOT (ahead-of-time), ta ścieżka zniknęła.
7. Niektóre zmiany składni wariantów#
Kilka wariantów zostało przemianowanych lub zmieniło zachowanie:
<!-- v3 -->
<div class="[&>*]:p-4">...</div>
<!-- v4 — część >* teraz używa składni wariantu inset -->
<div class="*:p-4">...</div>Składnia arbitralnych wariantów [&...] nadal działa, ale v4 dostarcza nazwane alternatywy dla typowych wzorców.
Przewodnik po migracji: Prawdziwy proces#
Oto jak faktycznie migrowałem — nie happy path z dokumentacji, ale jak ten proces naprawdę wyglądał.
Krok 1: Uruchom oficjalny codemod#
Tailwind dostarcza codemod, który obsługuje większość mechanicznych zmian:
npx @tailwindcss/upgradeRobi wiele automatycznie:
- Konwertuje dyrektywy
@tailwindna@import "tailwindcss" - Zmienia nazwy przestarzałych klas utility
- Aktualizuje składnię wariantów
- Konwertuje klasy opacity na składnię slash (
bg-opacity-50nabg-black/50) - Tworzy podstawowy blok
@themez twojej konfiguracji
Co codemod obsługuje dobrze#
- Zmiany nazw klas utility (prawie idealne)
- Zmiany składni dyrektyw
- Proste wartości motywu (kolory, odstępy, czcionki)
- Migracja składni opacity
Czego codemod NIE obsługuje#
- Złożone konwersje pluginów
- Dynamiczne wartości konfiguracji (wywołania
theme()w JavaScript) - Warunkowa konfiguracja motywu (np. wartości motywu oparte na zmiennych środowiskowych)
- Migracje niestandardowego API pluginów
- Edge case'y wartości arbitralnych, gdzie nowy parser interpretuje inaczej
- Nazwy klas konstruowane dynamicznie w JavaScript (template literals, konkatenacja stringów)
Krok 2: Napraw konfigurację PostCSS#
Dla większości setupów zaktualizujesz konfigurację PostCSS:
// postcss.config.js — v4
module.exports = {
plugins: {
"@tailwindcss/postcss": {},
},
};Uwaga: nazwa pluginu zmieniła się z tailwindcss na @tailwindcss/postcss. Jeśli używasz Vite, możesz całkowicie pominąć PostCSS i użyć pluginu Vite:
// vite.config.ts
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
plugins: [tailwindcss()],
});Krok 3: Konwertuj konfigurację motywu#
To jest część ręczna. Weź wartości motywu z tailwind.config.js i przekonwertuj je na @theme:
// konfiguracja v3 — przed
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" },
},
},
},
},
};/* CSS v4 — po */
@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; }
}Zwróć uwagę, że keyframes wychodzą poza @theme i stają się zwykłymi CSS @keyframes. Nazwa animacji w @theme po prostu się do nich odnosi. To czystsze — keyframes to CSS, powinny być pisane jako CSS.
Krok 4: Testy regresji wizualnej#
To nie podlega dyskusji. Po migracji otworzyłem każdą stronę mojej aplikacji i wizualnie ją sprawdziłem. Uruchomiłem też moje testy screenshot'ów w Playwright (jeśli je masz). Codemod jest dobry, ale nie idealny. Rzeczy, które wyłapałem w przeglądzie wizualnym:
- Kilka miejsc, gdzie migracja składni opacity dała nieco inne wyniki
- Wyjście niestandardowych pluginów, które się nie przeniosło
- Zmiany stosu z-index z powodu kolejności warstw
- Niektóre nadpisania
!important, które zachowywały się inaczej z cascade layers
Krok 5: Zaktualizuj zależności firm trzecich#
Sprawdź każdy pakiet związany z Tailwind:
{
"@tailwindcss/typography": "^1.0.0",
"@tailwindcss/forms": "^1.0.0",
"@tailwindcss/container-queries": "USUŃ — teraz wbudowane",
"tailwindcss-animate": "sprawdź wsparcie v4",
"prettier-plugin-tailwindcss": "zaktualizuj do najnowszej"
}Plugin @tailwindcss/container-queries nie jest już potrzebny — container queries są wbudowane. Inne pluginy wymagają wersji kompatybilnych z v4.
Praca z Next.js#
Ponieważ używam Next.js do większości projektów, oto konkretna konfiguracja.
Podejście PostCSS (zalecane dla Next.js)#
Next.js korzysta z PostCSS pod spodem, więc plugin PostCSS jest naturalnym wyborem:
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;
}To kompletny setup. Żadnego tailwind.config.js, żadnego autoprefixer (v4 obsługuje vendor prefixes wewnętrznie).
Kolejność importów CSS#
Jedna rzecz, która mnie potknęła: kolejność importów CSS ma większe znaczenie w v4 z powodu cascade layers. Twój @import "tailwindcss" powinien być przed twoimi niestandardowymi stylami:
/* prawidłowa kolejność */
@import "tailwindcss";
@import "./theme.css";
@import "./custom-utilities.css";
/* twoje inline @theme, @utility itp. */Jeśli zaimportujesz niestandardowy CSS przed Tailwind, twoje style mogą trafić do warstwy cascade o niższym priorytecie i zostać nieoczekiwanie nadpisane.
Dark mode#
Dark mode działa tak samo koncepcyjnie, ale konfiguracja przeniosła się do CSS:
@import "tailwindcss";
/* Użyj dark mode opartego na klasach (domyślny to oparty na media) */
@variant dark (&:where(.dark, .dark *));Zastępuje to konfigurację v3:
// v3
module.exports = {
darkMode: "class",
};Podejście z @variant jest bardziej elastyczne. Możesz zdefiniować dark mode jakkolwiek chcesz — oparty na klasach, na atrybutach data lub na media queries:
/* podejście z atrybutem data */
@variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));
/* media query — to jest domyślne, więc nie musisz tego deklarować */
@variant dark (@media (prefers-color-scheme: dark));Kompatybilność z Turbopack#
Jeśli używasz Next.js z Turbopackiem (który jest teraz domyślnym bundlerem dev), v4 działa świetnie. Silnik Rust dobrze współgra z architekturą Turbopacka opartą na Rust. Zmierzyłem czasy startu dev:
| Konfiguracja | v3 + Webpack | v3 + Turbopack | v4 + Turbopack |
|---|---|---|---|
| Zimny start | 4,8s | 2,1s | 1,3s |
| HMR (zmiana CSS) | 450ms | 180ms | 40ms |
40ms HMR dla zmian CSS to ledwo zauważalne. Wydaje się natychmiastowe.
Wydajność w głębszym spojrzeniu: Poza szybkością buildów#
Korzyści silnika Oxide wykraczają poza samą szybkość buildów.
Użycie pamięci#
v4 używa znacznie mniej pamięci. Na moim projekcie z 847 komponentami:
| Metryka | v3 | v4 |
|---|---|---|
| Szczytowa pamięć (build) | 380MB | 45MB |
| Stan ustalony (dev) | 210MB | 28MB |
To ma znaczenie dla pipeline'ów CI/CD, gdzie pamięć jest ograniczona, i dla maszyn deweloperskich uruchamiających dziesięć procesów jednocześnie.
Rozmiar wyjściowego CSS#
v4 generuje nieco mniejsze CSS, ponieważ nowy silnik jest lepszy w deduplikacji i eliminacji martwego kodu:
wyjście v3: 34,2 KB (gzipped)
wyjście v4: 29,8 KB (gzipped)
13% redukcji bez zmieniania żadnego kodu. Nie transformatywne, ale darmowa wydajność.
Tree shaking wartości motywu#
W v4, jeśli zdefiniujesz wartość motywu, ale nigdy jej nie użyjesz w swoich szablonach, odpowiednia CSS custom property i tak jest emitowana (jest w @theme, który mapuje się na zmienne :root). Jednak klasy utility dla nieużywanych wartości nie są generowane. To takie samo zachowanie jak JIT w v3, ale warto o tym wspomnieć: twoje CSS custom properties są zawsze dostępne, nawet dla wartości bez użycia w klasach utility.
Jeśli chcesz zapobiec generowaniu CSS custom properties przez pewne wartości motywu, możesz użyć @theme inline:
@theme inline {
/* Te wartości generują klasy utility, ale NIE CSS custom properties */
--color-internal-debug: #ff00ff;
--spacing-magic-number: 3.7rem;
}Przydaje się to dla wewnętrznych tokenów projektowych, których nie chcesz eksponować jako zmienne CSS.
Zaawansowane: Komponowanie motywów dla wielu marek#
Jeden wzorzec, który v4 znacznie ułatwia, to multi-brand theming. Ponieważ wartości motywu to CSS custom properties, możesz je podmieniać w runtime:
@import "tailwindcss";
@theme {
--color-brand: var(--brand-primary, #3b82f6);
--color-brand-light: var(--brand-light, #60a5fa);
--color-brand-dark: var(--brand-dark, #1d4ed8);
}
/* Nadpisania marek */
.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">
<!-- wszystkie bg-brand, text-brand itp. używają kolorów Acme -->
<div class="bg-brand text-white">Acme Corp</div>
</body>W v3 to wymagało niestandardowego pluginu lub złożonego setupu zmiennych CSS poza Tailwindem. W v4 jest to naturalne — motyw to zmienne CSS, a zmienne CSS się kaskadują. To rodzaj rzeczy, który sprawia, że podejście CSS-first wydaje się słuszne.
Czego mi brakuje z v3#
Bądźmy sprawiedliwi. Są rzeczy, które v3 robiło, a których naprawdę mi brakuje w v4:
1. Konfiguracja JavaScript dla programistycznych motywów. Miałem projekt, w którym generowaliśmy skale kolorów z jednego koloru marki za pomocą funkcji JavaScript w konfiguracji. W v4 nie możesz tego zrobić w @theme — potrzebujesz kroku budowania, który generuje plik CSS, albo obliczasz kolory raz i wklejasz je. Warstwa kompatybilności @config pomaga, ale nie jest to długoterminowe rozwiązanie.
2. IntelliSense było lepsze na starcie. Rozszerzenie VS Code dla v3 miało lata szlifowania. IntelliSense v4 działa, ale miało pewne luki na początku — niestandardowe wartości @theme czasami się nie podpowiadały, a definicje @utility nie zawsze były wykrywane. Znacznie się to poprawiło z ostatnimi aktualizacjami, ale warto o tym wspomnieć.
3. Dojrzałość ekosystemu. Ekosystem wokół v3 był ogromny. Headless UI, Radix, shadcn/ui, Flowbite, DaisyUI — wszystko było przetestowane pod v3. Wsparcie v4 jest wprowadzane, ale nie jest uniwersalne. Musiałem wysłać PR do jednej biblioteki komponentów, żeby naprawić kompatybilność z v4.
Czy powinieneś migrować?#
Oto moja ramka decyzyjna po kilku tygodniach życia z v4:
Migruj teraz jeśli:#
- Zaczynasz nowy projekt (oczywisty wybór — zacznij z v4)
- Twój projekt ma minimalne niestandardowe pluginy
- Chcesz korzyści wydajnościowych dla dużych projektów
- Już używasz nowoczesnych wzorców Tailwind (slash opacity,
shrink-*itd.) - Potrzebujesz container queries i wolisz nie dodawać pluginu
Poczekaj jeśli:#
- Mocno polegasz na wtyczkach Tailwind firm trzecich, które jeszcze nie wspierają v4
- Masz złożoną programistyczną konfigurację motywu
- Twój projekt jest stabilny i nie jest aktywnie rozwijany (po co go ruszać?)
- Jesteś w środku sprintu z funkcjami (migruj między sprintami, nie w trakcie)
Nie migruj jeśli:#
- Jesteś na v2 lub wcześniejszej (najpierw uaktualnij do v3, ustabilizuj, potem rozważ v4)
- Twój projekt kończy się za kilka miesięcy (nie warto wprowadzać zamieszania)
Moja szczera opinia#
Dla nowych projektów v4 to oczywisty wybór. Konfiguracja CSS-first jest czystsza, silnik jest dramatycznie szybszy, a nowe funkcje (container queries, @starting-style, nowe warianty) są naprawdę przydatne.
Dla istniejących projektów polecam podejście etapowe:
- Teraz: Zacznij każdy nowy projekt na v4
- Wkrótce: Eksperymentuj konwertując mały wewnętrzny projekt na v4
- Gdy gotowe: Migruj projekty produkcyjne podczas spokojnego sprintu, z testami regresji wizualnej
Migracja nie jest bolesna, jeśli się na nią przygotujesz. Codemod obsługuje 80% pracy. Pozostałe 20% jest ręczne, ale proste. Zaplanuj dzień na średni projekt, dwa do trzech dni na duży.
Tailwind v4 to to, czym Tailwind powinien był być od zawsze. Konfiguracja JavaScript była zawsze kompromisem wobec toolingu swoich czasów. Konfiguracja CSS-first, natywne cascade layers, silnik Rust — to nie trendy, to framework doganiający platformę. Platforma webowa stała się lepsza, a Tailwind v4 się w to wpisuje zamiast z tym walczyć.
Przejście do pisania tokenów projektowych w CSS, komponowania ich za pomocą funkcji CSS i pozwalania mechanizmowi kaskady przeglądarki na obsługę specyficzności — to właściwy kierunek. Potrzeba było czterech głównych wersji, aby tu dotrzeć, ale rezultatem jest najbardziej spójna wersja Tailwind do tej pory.
Zacznij swój następny projekt z nim. Nie będziesz się oglądać.