Tailwind CSS v4: Що реально змінилося і чи варто мігрувати
CSS-first конфігурація, інтеграція з @layer, вбудовані container queries, продуктивність нового рушія, breaking changes та мій чесний досвід міграції з v3 на v4.
Я використовую Tailwind CSS з версії v1.x, ще з тих часів, коли половина спільноти вважала його жахіттям, а інша половина не могла перестати шіпити з ним. Кожна мажорна версія була значним стрибком, але v4 — це інше. Це не просто релізz з фічами. Це архітектурний переписування з нуля, яке змінює фундаментальний контракт між тобою і фреймворком.
Після міграції двох продакшн-проєктів з v3 на v4 та старту трьох нових проєктів відразу на v4, я маю чітку картину того, що реально стало краще, що ще шорстке, і чи варто мігрувати сьогодні. Без хайпу, без обурення — тільки те, що я спостерігав.
Загальна картина: Що таке v4 насправді#
Tailwind CSS v4 — це три речі одночасно:
- Новий рушій — переписаний з JavaScript на Rust (рушій Oxide), що робить білди драматично швидшими
- Нова парадигма конфігурації — CSS-first конфігурація замінює
tailwind.config.jsяк дефолт - Тісніша інтеграція з CSS-платформою — нативні
@layer, container queries,@starting-styleта cascade layers стали першокласними громадянами
Заголовок, який ти побачиш всюди — "у 10 разів швидше." Це правда, але це применшує реальну зміну. Ментальна модель конфігурації та розширення Tailwind фундаментально зсунулась. Тепер ти працюєш з CSS, а не з JavaScript-об'єктом конфігурації, що генерує CSS.
Ось як виглядає мінімальне налаштування Tailwind v4:
/* app.css — this is the entire setup */
@import "tailwindcss";Все. Жодного конфіг-файлу. Жодної конфігурації PostCSS-плагіна (для більшості налаштувань). Жодних директив @tailwind base; @tailwind components; @tailwind utilities;. Один імпорт — і все працює.
Порівняй з 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: {},
},
};Три файли зведені до одного рядка. Це не просто менше бойлерплейту — це менша площа для помилок конфігурації. У v4 визначення контенту автоматичне. Воно сканує файли проєкту без потреби прописувати ґлоб-патерни.
CSS-First конфігурація з @theme#
Це найбільший концептуальний зсув. У v3 ти налаштовував Tailwind через 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",
},
},
},
};У v4 все це живе в CSS з використанням директиви @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;
}Спочатку я опирався цьому. Мені подобалось мати єдиний JavaScript-об'єкт, де я бачив всю свою дизайн-систему. Але через тиждень з CSS-підходом я змінив думку з трьох причин:
1. Нативні CSS custom properties автоматично стають доступними. Кожне значення, визначене в @theme, стає CSS custom property на :root. Це означає, що твої значення теми доступні у звичайному CSS, в CSS Modules, в тегах <style>, скрізь, де працює CSS:
/* you get this for free */
:root {
--color-brand-50: #eff6ff;
--color-brand-500: #3b82f6;
--color-brand-900: #1e3a5f;
}/* use them anywhere — no Tailwind needed */
.custom-element {
border: 2px solid var(--color-brand-500);
}2. Ти можеш використовувати CSS-фічі всередині @theme. Медіа-запити, light-dark(), calc() — справжній CSS працює тут, тому що це і є справжній CSS:
@theme {
--color-surface: light-dark(#ffffff, #0a0a0a);
--color-text: light-dark(#0a0a0a, #fafafa);
--spacing-container: calc(100vw - 2rem);
}3. Співрозміщення з іншим CSS. Твоя тема, твої кастомні утиліти і базові стилі живуть в одній мові, в одному файлі, якщо хочеш. Жодного перемикання контексту між "CSS-світом" та "JavaScript-конфіг-світом."
Перевизначення vs. Розширення дефолтної теми#
У v3 у тебе був theme (замінити) проти theme.extend (об'єднати). У v4 ментальна модель інша:
@import "tailwindcss";
/* This EXTENDS the default theme — adds brand colors alongside existing ones */
@theme {
--color-brand-500: #3b82f6;
}Якщо хочеш повністю замінити простір імен (наприклад, видалити всі дефолтні кольори), використовуєш @theme з wildcard-скиданням --color-*:
@import "tailwindcss";
@theme {
/* Clear all default colors first */
--color-*: initial;
/* Now define only your colors */
--color-white: #ffffff;
--color-black: #000000;
--color-brand-50: #eff6ff;
--color-brand-500: #3b82f6;
--color-brand-900: #1e3a5f;
}Цей патерн wildcard-скидання елегантний. Ти точно обираєш, які частини дефолтної теми залишити, а які замінити. Хочеш всі дефолтні відступи, але кастомні кольори? Скинь --color-*: initial; і залиш відступи як є.
Кілька файлів теми#
Для більших проєктів можна розділити тему по файлах:
/* 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";Це набагато чистіше за v3-патерн з гігантським tailwind.config.js або спробами розділити його через require().
Рушій Oxide: Реально у 10 разів швидше#
Рушій Tailwind v4 — це повне переписування на Rust. Вони називають його Oxide. Я скептично ставився до заяви "у 10 разів швидше" — маркетингові цифри рідко переживають зіткнення з реальними проєктами. Тому я провів бенчмарк.
Мій тестовий проєкт: Next.js-застосунок з 847 компонентами, 142 сторінками, приблизно 23 000 використань Tailwind-класів.
| Метрика | v3 (Node) | v4 (Oxide) | Покращення |
|---|---|---|---|
| Початковий білд | 4 280 мс | 387 мс | 11x |
| Інкрементальний (зміна 1 файлу) | 340 мс | 18 мс | 19x |
| Повний ребілд (чистий) | 5 100 мс | 510 мс | 10x |
| Старт dev-сервера | 3 200 мс | 290 мс | 11x |
Заява "10x" для мого проєкту виявилась консервативною. Інкрементальні білди — це де воно реально блищить — 18 мс означає, що це по суті миттєво. Ти зберігаєш файл, і браузер має нові стилі раніше, ніж ти встигнеш переключити вкладку.
Чому він настільки швидший?#
Три причини:
1. Rust замість JavaScript. Ядро CSS-парсера, визначення класів та генерація коду — все нативний Rust. Це не ситуація "давайте перепишемо на Rust заради розваги" — парсинг CSS реально є CPU-bound роботою, де нативний код має масивну перевагу над V8.
2. Жодного PostCSS на гарячому шляху. У v3 Tailwind був PostCSS-плагіном. Кожен білд означав: розпарсити CSS в PostCSS AST, запустити Tailwind-плагін, серіалізувати назад у CSS-рядок, потім інші PostCSS-плагіни. У v4 Tailwind має власний CSS-парсер, що йде напряму від джерела до виходу. PostCSS все ще підтримується для сумісності, але основний шлях його повністю обходить.
3. Розумніша інкрементальна обробка. Новий рушій агресивно кешує. Коли ти редагуєш один файл, він пересканує лише цей файл на класи і регенерує лише змінені CSS-правила. V3-рушій був розумнішим щодо цього, ніж люди думають (JIT-режим вже був інкрементальним), але v4 просуває це набагато далі з дрібнозернистим відстеженням залежностей.
Чи реально швидкість має значення?#
Так, але не з тієї причини, яку ти б очікував. Для більшості проєктів швидкість білду v3 була "нормальною." Ти чекав кілька сотень мілісекунд у dev. Не болісно.
Швидкість v4 має значення, тому що робить Tailwind невидимим у твоєму тулчейні. Коли білди займають менше 20 мс, ти перестаєш думати про Tailwind як про крок білду взагалі. Він стає як підсвітка синтаксису — завжди є, ніколи не заважає. Ця психологічна різниця суттєва протягом цілого дня розробки.
Нативна інтеграція @layer#
У v3 Tailwind використовував власну систему шарів з @layer base, @layer components та @layer utilities. Вони виглядали як CSS cascade layers, але ними не були — це були Tailwind-специфічні директиви, що контролювали, де згенерований CSS з'являється у виході.
У v4 Tailwind використовує справжні CSS cascade layers:
/* v4 output — simplified */
@layer theme, base, components, utilities;
@layer base {
/* reset, preflight */
}
@layer components {
/* your component classes */
}
@layer utilities {
/* all generated utility classes */
}Це суттєва зміна, тому що CSS cascade layers мають реальні наслідки для специфічності. Правило в шарі з нижчим пріоритетом завжди програє правилу в шарі з вищим пріоритетом, незалежно від специфічності селектора. Це означає:
@layer components {
/* specificity: 0-1-0 */
.card { padding: 1rem; }
}
@layer utilities {
/* specificity: 0-1-0 — same specificity but wins because utilities layer is later */
.p-4 { padding: 1rem; }
}Утиліти завжди перевизначають компоненти. Компоненти завжди перевизначають base. Так Tailwind працював концептуально в v3, але тепер це забезпечується механізмом cascade layers браузера, а не маніпуляцією порядком джерела.
Додавання кастомних утиліт#
У v3 ти визначав кастомні утиліти через plugin API або @layer utilities:
// v3 — plugin approach
const plugin = require("tailwindcss/plugin");
module.exports = {
plugins: [
plugin(function ({ addUtilities }) {
addUtilities({
".text-balance": {
"text-wrap": "balance",
},
".text-pretty": {
"text-wrap": "pretty",
},
});
}),
],
};У v4 кастомні утиліти визначаються директивою @utility:
@import "tailwindcss";
@utility text-balance {
text-wrap: balance;
}
@utility text-pretty {
text-wrap: pretty;
}Директива @utility каже Tailwind "це утилітарний клас — постав його в шар utilities і дозволь використовувати з варіантами." Остання частина ключова. Утиліта, визначена через @utility, автоматично працює з hover:, focus:, md: та кожним іншим варіантом:
<p class="text-pretty md:text-balance">...</p>Кастомні варіанти#
Ти також можеш визначати кастомні варіанти через @variant:
@import "tailwindcss";
@variant hocus (&:hover, &:focus);
@variant theme-dark (.dark &);<button class="hocus:bg-brand-500 theme-dark:text-white">
Click me
</button>Це замінює v3 addVariant plugin API для більшості випадків використання. Воно менш потужне (ти не можеш програмно генерувати варіанти), але покриває 90% того, що люди реально роблять.
Container Queries: Вбудовані, без плагіна#
Container queries були однією з найбільш запитуваних фіч у v3. Їх можна було отримати з плагіном @tailwindcss/container-queries, але це був додаток. У v4 вони вбудовані у фреймворк.
Базове використання#
Позначай контейнер через @container і запитуй його розмір з префіксом @:
<!-- mark parent as a container -->
<div class="@container">
<!-- responsive to parent's width, not 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>Варіанти @md, @lg тощо працюють як адаптивні брейкпоінти, але відносно найближчого предка @container, а не в'юпорту. Значення брейкпоінтів відповідають дефолтним брейкпоінтам Tailwind:
| Варіант | Мін. ширина |
|---|---|
@sm | 24rem (384px) |
@md | 28rem (448px) |
@lg | 32rem (512px) |
@xl | 36rem (576px) |
@2xl | 42rem (672px) |
Іменовані контейнери#
Ти можеш іменувати контейнери для запитів до конкретних предків:
<div class="@container/sidebar">
<div class="@container/card">
<!-- queries the card container -->
<div class="@md/card:text-lg">...</div>
<!-- queries the sidebar container -->
<div class="@lg/sidebar:hidden">...</div>
</div>
</div>Чому це важливо#
Container queries змінюють те, як ти думаєш про адаптивний дизайн. Замість "при такій ширині в'юпорту показати три колонки" ти кажеш "коли контейнер цього компонента достатньо широкий, показати три колонки." Компоненти стають по-справжньому самодостатніми. Ти можеш перемістити компонент картки з повноширинного лейауту в сайдбар — і він автоматично адаптується. Жодних акробатик з медіа-запитами.
Я рефакторю свої бібліотеки компонентів, щоб використовувати container queries за замовчуванням замість в'юпорт-брейкпоінтів. Результат — компоненти, що працюють скрізь, куди їх поставиш, без необхідності для батьківського елемента знати щось про адаптивну поведінку компонента.
<!-- This component adapts to ANY container it's placed in -->
<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>Нові варіанти, що реально мають значення#
v4 додає кілька нових варіантів, до яких я постійно тягнуся. Вони заповнюють реальні прогалини.
Варіант starting:#
Він відповідає CSS @starting-style, який дозволяє визначити початковий стан елемента при його першій появі. Це відсутня деталь для анімації появи елемента без JavaScript:
<dialog class="opacity-0 starting:opacity-0 open:opacity-100 transition-opacity duration-300">
<p>This dialog fades in when opened</p>
</dialog>Варіант starting: генерує CSS всередині блоку @starting-style:
/* what Tailwind generates */
@starting-style {
dialog[open] {
opacity: 0;
}
}
dialog[open] {
opacity: 1;
transition: opacity 300ms;
}Це величезно для діалогів, поповерів, дропдаун-меню — для всього, що потребує анімації появи. Раніше для цього потрібен був JavaScript, щоб додати клас на наступному фреймі, або ти використовував @keyframes. Тепер це утилітарний клас.
Варіант not-*#
Заперечення. Те, чого ми хотіли завжди:
<!-- every child except the last gets a 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 everything that's not disabled -->
<input class="not-disabled:hover:border-brand-500" />
<!-- negate data attributes -->
<div class="not-data-active:opacity-50">...</div>Варіанти nth-*#
Прямий доступ до nth-child та nth-of-type:
<ul>
<li class="nth-1:font-bold">First item — bold</li>
<li class="nth-even:bg-gray-50">Even rows — gray bg</li>
<li class="nth-odd:bg-white">Odd rows — white bg</li>
<li class="nth-[3n+1]:text-brand-500">Every third+1 — brand color</li>
</ul>Синтаксис з дужками (nth-[3n+1]) підтримує будь-який валідний вираз nth-child. Це замінює багато кастомного CSS, який я раніше писав для смуг у таблицях та патернів у грідах.
Варіант in-* (Стан батька)#
Це зворотний бік group-*. Замість "коли мій батько (група) в стані hover, стилізуй мене" — це "коли я всередині батька, що відповідає цьому стану, стилізуй мене":
<div class="in-data-active:bg-brand-50">
This gets a background when any ancestor has data-active
</div>Варіант **: для глибокої універсальної вибірки#
Стилізуй всіх нащадків, а не лише прямих дітей. Це контрольована потужність — використовуй обережно, але для prose-контенту та виходу CMS це безцінно:
<!-- all paragraphs inside this div, at any depth -->
<div class="**:data-highlight:bg-yellow-100">
<section>
<p data-highlight>This gets highlighted</p>
<div>
<p data-highlight>So does this, nested deeper</p>
</div>
</section>
</div>Breaking Changes: Що реально зламалось#
Скажу прямо. Якщо у тебе великий v3-проєкт, міграція нетривіальна. Ось що зламалось у моїх проєктах:
1. Формат конфігурації#
Твій tailwind.config.js не працює з коробки. Потрібно або:
- Конвертувати його в CSS
@theme(рекомендовано для нової архітектури) - Використати шар сумісності з директивою
@config(швидкий шлях міграції)
/* quick migration — keep your old config */
@import "tailwindcss";
@config "../../tailwind.config.js";Цей міст @config працює, але він явно є інструментом міграції. Рекомендація — з часом перейти на @theme.
2. Видалені застарілі утиліти#
Деякі утиліти, що були deprecated у v3, зникли:
/* REMOVED in v4 */
bg-opacity-* → use bg-black/50 (slash opacity syntax)
text-opacity-* → use text-black/50
border-opacity-* → use border-black/50
flex-shrink-* → use shrink-*
flex-grow-* → use grow-*
overflow-ellipsis → use text-ellipsis
decoration-slice → use box-decoration-slice
decoration-clone → use box-decoration-clone
Якщо ти вже використовував сучасний синтаксис у v3 (слеш-прозорість, shrink-*), все добре. Якщо ні — це прості зміни find-and-replace.
3. Зміни палітри кольорів за замовчуванням#
Палітра кольорів за замовчуванням трохи зсунулась. Якщо ти залежиш від точних значень кольорів з v3 (не за іменем, а за реальним hex-значенням), можеш помітити візуальні різниці. Іменовані кольори (blue-500, gray-200) все ще існують, але деякі hex-значення змінились.
4. Визначення контенту#
v3 вимагав явної конфігурації content:
// v3
module.exports = {
content: [
"./src/**/*.{js,ts,jsx,tsx}",
"./components/**/*.{js,ts,jsx,tsx}",
],
};v4 використовує автоматичне визначення контенту. Він сканує корінь проєкту та знаходить файли шаблонів автоматично. Це здебільшого "просто працює", але якщо у тебе незвична структура проєкту (монорепо з пакетами поза коренем проєкту, файли шаблонів у неочікуваних місцях), може знадобитися явно налаштувати шляхи:
@import "tailwindcss";
@source "../shared-components/**/*.tsx";
@source "../design-system/src/**/*.tsx";5. Зміни Plugin API#
Якщо ти писав кастомні плагіни, API змінився. Функції addUtilities, addComponents, addBase та addVariant все ще працюють через шар сумісності, але ідіоматичний підхід v4 — CSS-нативний:
// v3 plugin
plugin(function ({ addUtilities, theme }) {
addUtilities({
".scrollbar-hide": {
"-ms-overflow-style": "none",
"scrollbar-width": "none",
"&::-webkit-scrollbar": {
display: "none",
},
},
});
});/* v4 — just CSS */
@utility scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}Більшість офіційних плагінів (@tailwindcss/typography, @tailwindcss/forms, @tailwindcss/aspect-ratio) мають v4-сумісні версії. Сторонні плагіни — як пощастить — перевіряй їхні репозиторії перед міграцією.
6. JIT — єдиний режим#
У v3 можна було відмовитись від JIT-режиму (хоча майже ніхто цього не робив). У v4 не-JIT режиму немає. Все генерується на вимогу, завжди. Якщо у тебе була якась причина використовувати старий AOT (ahead-of-time) рушій, цей шлях зник.
7. Деякі зміни синтаксису варіантів#
Кілька варіантів були перейменовані або змінили поведінку:
<!-- v3 -->
<div class="[&>*]:p-4">...</div>
<!-- v4 — the >* part now uses the inset variant syntax -->
<div class="*:p-4">...</div>Синтаксис довільних варіантів [&...] все ще працює, але v4 надає іменовані альтернативи для поширених патернів.
Гайд з міграції: Реальний процес#
Ось як я реально мігрував — не щасливий шлях з документації, а як процес виглядав насправді.
Крок 1: Запустити офіційний кодмод#
Tailwind надає кодмод, що обробляє більшість механічних змін:
npx @tailwindcss/upgradeВін робить багато автоматично:
- Конвертує директиви
@tailwindв@import "tailwindcss" - Перейменовує застарілі утилітарні класи
- Оновлює синтаксис варіантів
- Конвертує утиліти прозорості в слеш-синтаксис (
bg-opacity-50вbg-black/50) - Створює базовий блок
@themeз твого конфігу
Що кодмод обробляє добре#
- Перейменування утилітарних класів (майже ідеально)
- Зміни синтаксису директив
- Прості значення теми (кольори, відступи, шрифти)
- Міграція синтаксису прозорості
Що кодмод НЕ обробляє#
- Складні конвертації плагінів
- Динамічні значення конфігу (виклики
theme()в JavaScript) - Умовну конфігурацію теми (наприклад, значення на основі середовища)
- Міграцію кастомного plugin API
- Крайні випадки довільних значень, де новий парсер інтерпретує інакше
- Імена класів, побудовані динамічно в JavaScript (шаблонні літерали, конкатенація рядків)
Крок 2: Виправити конфігурацію PostCSS#
Для більшості налаштувань оновиш свій PostCSS-конфіг:
// postcss.config.js — v4
module.exports = {
plugins: {
"@tailwindcss/postcss": {},
},
};Зверни увагу: назва плагіна змінилась з tailwindcss на @tailwindcss/postcss. Якщо використовуєш Vite, можна взагалі пропустити PostCSS і використати Vite-плагін:
// vite.config.ts
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
plugins: [tailwindcss()],
});Крок 3: Конвертувати конфігурацію теми#
Це ручна частина. Бери значення теми з tailwind.config.js і конвертуй їх в @theme:
// v3 config — before
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 — after */
@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; }
}Зверни увагу, що keyframes виносяться за межі @theme і стають звичайними CSS @keyframes. Назва анімації в @theme просто посилається на них. Це чистіше — keyframes це CSS, і їх слід писати як CSS.
Крок 4: Візуальне регресійне тестування#
Це не обговорюється. Після міграції я відкрив кожну сторінку свого застосунку та візуально перевірив. Також запустив свої Playwright-скріншот-тести (якщо вони є). Кодмод хороший, але не ідеальний. Що я зловив при візуальному огляді:
- Кілька місць, де міграція синтаксису прозорості дала трохи інші результати
- Вивід кастомних плагінів, що не перенісся
- Зміни z-index стекінгу через порядок шарів
- Деякі
!importantoverrides, що поводились інакше з cascade layers
Крок 5: Оновити сторонні залежності#
Перевір кожен Tailwind-пакет:
{
"@tailwindcss/typography": "^1.0.0",
"@tailwindcss/forms": "^1.0.0",
"@tailwindcss/container-queries": "REMOVE — built-in now",
"tailwindcss-animate": "check for v4 support",
"prettier-plugin-tailwindcss": "update to latest"
}Плагін @tailwindcss/container-queries більше не потрібен — container queries вбудовані. Інші плагіни потребують своїх v4-сумісних версій.
Робота з Next.js#
Оскільки я використовую Next.js для більшості проєктів, ось конкретне налаштування.
Підхід PostCSS (Рекомендовано для Next.js)#
Next.js використовує PostCSS під капотом, тому PostCSS-плагін — природний вибір:
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;
}Це повне налаштування. Жодного tailwind.config.js, жодного autoprefixer (v4 обробляє вендорні префікси внутрішньо).
Порядок імпортів CSS#
Одна річ, що мене спіткнула: порядок імпортів CSS має більше значення у v4 через cascade layers. Твій @import "tailwindcss" має йти перед кастомними стилями:
/* correct order */
@import "tailwindcss";
@import "./theme.css";
@import "./custom-utilities.css";
/* your inline @theme, @utility, etc. */Якщо імпортуєш кастомний CSS перед Tailwind, твої стилі можуть опинитися в шарі з нижчим пріоритетом і бути неочікувано перевизначені.
Темний режим#
Темний режим працює концептуально так само, але конфігурація переїхала в CSS:
@import "tailwindcss";
/* Use class-based dark mode (default is media-based) */
@variant dark (&:where(.dark, .dark *));Це замінює конфіг v3:
// v3
module.exports = {
darkMode: "class",
};Підхід @variant гнучкіший. Ти можеш визначити темний режим як хочеш — на основі класу, data-атрибуту або медіа-запиту:
/* data attribute approach */
@variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));
/* media query — this is the default, so you don't need to declare it */
@variant dark (@media (prefers-color-scheme: dark));Сумісність з Turbopack#
Якщо ти використовуєш Next.js з Turbopack (який тепер є дефолтним dev-бандлером), v4 працює чудово. Rust-рушій добре поєднується з власною Rust-архітектурою Turbopack. Я виміряв час старту dev-сервера:
| Налаштування | v3 + Webpack | v3 + Turbopack | v4 + Turbopack |
|---|---|---|---|
| Холодний старт | 4.8 с | 2.1 с | 1.3 с |
| HMR (зміна CSS) | 450 мс | 180 мс | 40 мс |
40 мс HMR для CSS-змін ледве відчутно. Відчувається миттєво.
Глибокий розбір продуктивності: Більше ніж швидкість білду#
Переваги рушія Oxide виходять за рамки чистої швидкості білду.
Використання пам'яті#
v4 використовує суттєво менше пам'яті. На моєму проєкті з 847 компонентами:
| Метрика | v3 | v4 |
|---|---|---|
| Пікова пам'ять (білд) | 380 МБ | 45 МБ |
| Стабільний стан (dev) | 210 МБ | 28 МБ |
Це має значення для CI/CD пайплайнів з обмеженою пам'яттю та для машин розробки, що запускають десять процесів одночасно.
Розмір CSS-виходу#
v4 генерує трохи менший CSS-вихід, бо новий рушій краще дедуплікує та видаляє мертвий код:
v3 output: 34.2 KB (gzipped)
v4 output: 29.8 KB (gzipped)
Зменшення на 13% без зміни коду. Не трансформаційно, але безкоштовна продуктивність.
Tree Shaking значень теми#
У v4, якщо ти визначаєш значення теми, але ніколи не використовуєш його в шаблонах, відповідна CSS custom property все одно емітується (вона в @theme, що відповідає змінним :root). Однак утилітарні класи для невикористаних значень не генеруються. Це так само, як JIT-поведінка v3, але варто зазначити: твої CSS custom properties завжди доступні, навіть для значень без використання утиліт.
Якщо хочеш запобігти генерації CSS custom properties для певних значень теми, можеш використати @theme inline:
@theme inline {
/* These values generate utilities but NOT CSS custom properties */
--color-internal-debug: #ff00ff;
--spacing-magic-number: 3.7rem;
}Це корисно для внутрішніх дизайн-токенів, які ти не хочеш виставляти як CSS-змінні.
Просунуте: Композиція тем для мультибренду#
Один патерн, який v4 робить значно легшим — мультибрендова тематизація. Оскільки значення теми є CSS custom properties, ти можеш підміняти їх у рантаймі:
@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">
<!-- all bg-brand, text-brand, etc. use Acme colors -->
<div class="bg-brand text-white">Acme Corp</div>
</body>У v3 це вимагало кастомного плагіна або складного налаштування CSS-змінних поза Tailwind. У v4 це природно — тема це CSS-змінні, а CSS-змінні каскадуються. Це той вид речей, що робить CSS-first підхід правильним.
Чого мені не вистачає від v3#
Буду збалансованим. Є речі, які v3 робив і яких мені реально не вистачає у v4:
1. JavaScript-конфіг для програмних тем. У мене був проєкт, де ми генерували шкали кольорів з одного бренд-кольору за допомогою JavaScript-функції в конфізі. У v4 це неможливо в @theme — потрібен крок білду, що генерує CSS-файл, або ти обчислюєш кольори один раз і вставляєш їх. Шар сумісності @config допомагає, але це не довгострокова історія.
2. IntelliSense був кращим на старті. Розширення v3 для VS Code мало роки полірування. IntelliSense v4 працює, але на початку мав прогалини — кастомні значення @theme іноді не автодоповнювались, а визначення @utility не завжди підхоплювались. Це суттєво покращилось з недавніми оновленнями, але варто зазначити.
3. Зрілість екосистеми. Екосистема навколо v3 була величезною. Headless UI, Radix, shadcn/ui, Flowbite, DaisyUI — все було протестовано проти v3. Підтримка v4 розгортається, але не універсальна. Мені довелось надіслати PR в одну бібліотеку компонентів, щоб виправити сумісність з v4.
Чи варто мігрувати?#
Ось моя система прийняття рішень після кількох тижнів з v4:
Мігруй зараз, якщо:#
- Починаєш новий проєкт (очевидний вибір — стартуй з v4)
- Твій проєкт має мінімум кастомних плагінів
- Хочеш переваги продуктивності для великих проєктів
- Вже використовуєш сучасні патерни Tailwind (слеш-прозорість,
shrink-*тощо) - Тобі потрібні container queries і не хочеться додавати плагін
Зачекай, якщо:#
- Сильно залежиш від сторонніх Tailwind-плагінів, що ще не підтримують v4
- Маєш складну програмну конфігурацію теми
- Проєкт стабільний і не розвивається активно (навіщо чіпати?)
- Ти посеред спринту з фічами (мігруй між спринтами, не під час)
Не мігруй, якщо:#
- Ти на v2 або раніше (спочатку апгрейдни до v3, стабілізуй, потім розглядай v4)
- Проєкт закінчується через кілька місяців (не варто метушні)
Моя чесна думка#
Для нових проєктів v4 — очевидний вибір. CSS-first конфігурація чистіша, рушій драматично швидший, і нові фічі (container queries, @starting-style, нові варіанти) реально корисні.
Для існуючих проєктів я рекомендую поетапний підхід:
- Зараз: Стартуй будь-який новий проєкт на v4
- Скоро: Поекспериментуй, конвертувавши маленький внутрішній проєкт на v4
- Коли готовий: Мігруй продакшн-проєкти під час спокійного спринту з візуальним регресійним тестуванням
Міграція не болісна, якщо ти до неї підготувався. Кодмод обробляє 80% роботи. Решта 20% — ручна, але прямолінійна. Закладай день на середній проєкт, два-три дні на великий.
Tailwind v4 — це те, чим Tailwind мав бути з самого початку. JavaScript-конфігурація завжди була поступкою інструментарію свого часу. CSS-first конфігурація, нативні cascade layers, Rust-рушій — це не тренди, це фреймворк, що наздоганяє платформу. Веб-платформа стала кращою, і Tailwind v4 спирається на неї замість того, щоб боротися.
Крок до написання дизайн-токенів у CSS, їх композиції з CSS-фічами та дозволу браузерному каскаду обробляти специфічність — це правильний напрямок. Знадобилось чотири мажорні версії, щоб сюди дійти, але результат — найбільш цілісна версія Tailwind на сьогодні.
Стартуй свій наступний проєкт з ним. Не пожалкуєш.