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.
Tailwind CSS v4 to trzy rzeczy naraz:
tailwind.config.js jako domyślną@layer, container queries, @starting-style i cascade layers to obywatele pierwszej klasyNagłó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.
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."
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.
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 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ę.
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.
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.
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.
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>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 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.
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) |
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>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>v4 dodaje kilka nowych wariantów, po które ciągle sięgam. Wypełniają realne luki.
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.
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>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.
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>**: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>Powiem wprost. Jeśli masz duży projekt v3, migracja nie jest trywialna. Oto co się zepsuło w moich projektach:
Twój tailwind.config.js nie działa od razu. Musisz albo:
@theme (zalecane dla nowej architektury)@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.
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ń.
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.
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";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ą.
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.
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.
Oto jak faktycznie migrowałem — nie happy path z dokumentacji, ale jak ten proces naprawdę wyglądał.
Tailwind dostarcza codemod, który obsługuje większość mechanicznych zmian:
npx @tailwindcss/upgradeRobi wiele automatycznie:
@tailwind na @import "tailwindcss"bg-opacity-50 na bg-black/50)@theme z twojej konfiguracjitheme() w JavaScript)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()],
});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.
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:
!important, które zachowywały się inaczej z cascade layersSprawdź 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.
Ponieważ używam Next.js do większości projektów, oto konkretna konfiguracja.
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).
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 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));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.
Korzyści silnika Oxide wykraczają poza samą szybkość buildów.
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.
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ść.
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.
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.
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.
Oto moja ramka decyzyjna po kilku tygodniach życia z v4:
shrink-* itd.)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:
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ć.