Tailwind CSS v4: что реально изменилось и стоит ли мигрировать
CSS-first конфигурация, интеграция с @layer, встроенные container queries, производительность нового движка, ломающие изменения и мой честный опыт миграции с v3 на v4.
Я использую Tailwind CSS начиная с v1.x, ещё в те времена, когда половина сообщества считала его уродством, а другая половина не могла остановиться и продолжала выпускать продукты с ним. Каждая мажорная версия была значительным скачком, но v4 — это другое. Это не просто релиз с новыми функциями. Это архитектурное переписывание с нуля, которое меняет фундаментальный контракт между вами и фреймворком.
После миграции двух продакшн-проектов с v3 на v4 и запуска трёх новых проектов на v4 с чистого листа у меня сложилась ясная картина того, что действительно стало лучше, что ещё сыровато и стоит ли мигрировать прямо сейчас. Без хайпа, без возмущений — только то, что я наблюдал.
Общая картина: что такое v4 на самом деле#
Tailwind CSS v4 — это три вещи одновременно:
- Новый движок — переписан с JavaScript на Rust (движок Oxide), что кардинально ускоряет сборку
- Новая парадигма конфигурации — CSS-first конфигурация заменяет
tailwind.config.jsв качестве варианта по умолчанию - Более тесная интеграция с CSS-платформой — нативные
@layer, container queries,@starting-styleи каскадные слои стали первоклассными сущностями
Заголовок, который вы увидите везде — «в 10 раз быстрее». Это правда, но это преуменьшает реальные изменения. Ментальная модель настройки и расширения Tailwind фундаментально изменилась. Теперь вы работаете с CSS, а не с JavaScript-объектом конфигурации, который генерирует CSS.
Вот как выглядит минимальная настройка Tailwind v4:
/* app.css — это вся настройка целиком */
@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 обнаружение контента происходит автоматически. Фреймворк сканирует файлы вашего проекта, и вам не нужно выписывать glob-паттерны.
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:
/* это вы получаете бесплатно */
:root {
--color-brand-50: #eff6ff;
--color-brand-500: #3b82f6;
--color-brand-900: #1e3a5f;
}/* используйте где угодно — Tailwind не нужен */
.custom-element {
border: 2px solid var(--color-brand-500);
}2. Внутри @theme можно использовать CSS-фичи. Media queries, 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";
/* Это РАСШИРЯЕТ тему по умолчанию — добавляет brand-цвета наряду с существующими */
@theme {
--color-brand-500: #3b82f6;
}Если вы хотите полностью заменить пространство имён (например, убрать все цвета по умолчанию), используйте @theme с wildcard-сбросом --color-*:
@import "tailwindcss";
@theme {
/* Сначала очищаем все цвета по умолчанию */
--color-*: initial;
/* Теперь определяем только свои цвета */
--color-white: #ffffff;
--color-black: #000000;
--color-brand-50: #eff6ff;
--color-brand-500: #3b82f6;
--color-brand-900: #1e3a5f;
}Этот паттерн с wildcard-сбросом элегантен. Вы сами выбираете, какие части темы по умолчанию оставить, а какие заменить. Хотите все стандартные отступы, но кастомные цвета? Сбросьте --color-*: initial; и оставьте spacing как есть.
Множественные файлы темы#
Для крупных проектов можно разделить тему на несколько файлов:
/* 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-нагруженная работа, где нативный код имеет колоссальное преимущество перед V8.
2. PostCSS убран из горячего пути. В v3 Tailwind был PostCSS-плагином. Каждая сборка означала: разбор CSS в AST PostCSS, запуск плагина 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, но ими не были — это были специфичные для Tailwind директивы, управляющие расположением сгенерированного CSS в выходном файле.
В v4 Tailwind использует настоящие каскадные слои CSS:
/* v4 вывод — упрощённо */
@layer theme, base, components, utilities;
@layer base {
/* reset, preflight */
}
@layer components {
/* ваши классы компонентов */
}
@layer utilities {
/* все сгенерированные классы утилит */
}Это значительное изменение, потому что каскадные слои CSS имеют реальные последствия для специфичности. Правило в слое с более низким приоритетом всегда проигрывает правилу в слое с более высоким приоритетом, независимо от специфичности селектора. Это означает:
@layer components {
/* специфичность: 0-1-0 */
.card { padding: 1rem; }
}
@layer utilities {
/* специфичность: 0-1-0 — та же специфичность, но побеждает, потому что слой utilities идёт позже */
.p-4 { padding: 1rem; }
}Утилиты всегда перекрывают компоненты. Компоненты всегда перекрывают базу. Именно так Tailwind работал концептуально в v3, но теперь это обеспечивается браузерным механизмом каскадных слоёв, а не манипуляцией порядком расположения в исходном коде.
Добавление кастомных утилит#
В v3 кастомные утилиты определялись через API плагинов или @layer utilities:
// v3 — подход с плагинами
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">
Нажми меня
</button>Это заменяет API плагинов addVariant из v3 для большинства случаев. Он менее мощный (нельзя делать программную генерацию вариантов), но покрывает 90% того, что люди реально делают.
Container queries: встроенные, без плагина#
Container queries были одной из самых запрашиваемых функций в v3. Их можно было получить с плагином @tailwindcss/container-queries, но это было дополнение. В v4 они встроены во фреймворк.
Базовое использование#
Отметьте контейнер с помощью @container и запрашивайте его размер с префиксом @:
<!-- помечаем родителя как контейнер -->
<div class="@container">
<!-- адаптивность к ширине родителя, а не вьюпорта -->
<div class="flex flex-col @md:flex-row @lg:grid @lg:grid-cols-3">
<div class="p-4">Карточка 1</div>
<div class="p-4">Карточка 2</div>
<div class="p-4">Карточка 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">
<!-- запрос к контейнеру card -->
<div class="@md/card:text-lg">...</div>
<!-- запрос к контейнеру sidebar -->
<div class="@lg/sidebar:hidden">...</div>
</div>
</div>Почему это важно#
Container queries меняют подход к адаптивному дизайну. Вместо «при такой ширине вьюпорта показать три колонки» вы говорите «когда контейнер этого компонента достаточно широк, показать три колонки». Компоненты становятся по-настоящему самодостаточными. Вы можете переместить карточку из полноширинного макета в сайдбар, и она автоматически адаптируется. Никакой гимнастики с media queries.
Я рефакторил свои библиотеки компонентов, чтобы использовать container queries по умолчанию вместо брейкпоинтов вьюпорта. Результат — компоненты, которые работают везде, куда бы вы их ни поместили, без необходимости родителю знать что-либо об адаптивном поведении компонента.
<!-- Этот компонент адаптируется к ЛЮБОМУ контейнеру, в который его поместят -->
<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">Заголовок поста</h2>
<p class="mt-2 text-sm @md:text-base text-gray-600">
Отрывок поста здесь...
</p>
<div class="mt-4 hidden @md:flex gap-2">
<span class="text-xs bg-gray-100 px-2 py-1 rounded">Тег</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>Этот диалог плавно появляется при открытии</p>
</dialog>Вариант starting: генерирует CSS внутри блока @starting-style:
/* что генерирует Tailwind */
@starting-style {
dialog[open] {
opacity: 0;
}
}
dialog[open] {
opacity: 1;
transition: opacity 300ms;
}Это огромный шаг для диалогов, поповеров, выпадающих меню — всего, что нуждается в анимации появления. Раньше для этого нужен был JavaScript, добавляющий класс на следующем кадре, или вы использовали @keyframes. Теперь это утилитарный класс.
Вариант not-*#
Отрицание. То, чего мы хотели всегда:
<!-- каждый дочерний элемент, кроме последнего, получает рамку -->
<div class="divide-y">
<div class="not-last:pb-4">Элемент 1</div>
<div class="not-last:pb-4">Элемент 2</div>
<div class="not-last:pb-4">Элемент 3</div>
</div>
<!-- стилизовать всё, что не disabled -->
<input class="not-disabled:hover:border-brand-500" />
<!-- отрицание data-атрибутов -->
<div class="not-data-active:opacity-50">...</div>Варианты nth-*#
Прямой доступ к nth-child и nth-of-type:
<ul>
<li class="nth-1:font-bold">Первый элемент — жирный</li>
<li class="nth-even:bg-gray-50">Чётные строки — серый фон</li>
<li class="nth-odd:bg-white">Нечётные строки — белый фон</li>
<li class="nth-[3n+1]:text-brand-500">Каждый третий+1 — brand-цвет</li>
</ul>Синтаксис со скобками (nth-[3n+1]) поддерживает любое допустимое выражение nth-child. Это заменяет множество кастомного CSS, который я раньше писал для полосатых таблиц и паттернов сеток.
Вариант in-* (состояние родителя)#
Это обратная сторона group-*. Вместо «когда мой родитель (group) находится в hover, стилизовать меня» — это «когда я внутри родителя, который соответствует этому состоянию, стилизовать меня»:
<div class="in-data-active:bg-brand-50">
Этот элемент получает фон, когда у любого предка есть data-active
</div>Глубокий универсальный вариант **:#
Стилизация всех потомков, а не только прямых дочерних элементов. Это контролируемая мощь — используйте с осторожностью, но это незаменимо для прозаического контента и CMS-вывода:
<!-- все параграфы внутри этого div, на любой глубине -->
<div class="**:data-highlight:bg-yellow-100">
<section>
<p data-highlight>Это будет подсвечено</p>
<div>
<p data-highlight>И это тоже, вложенное глубже</p>
</div>
</section>
</div>Ломающие изменения: что реально сломалось#
Скажу прямо. Если у вас большой проект на v3, миграция не тривиальна. Вот что сломалось в моих проектах:
1. Формат конфигурации#
Ваш tailwind.config.js не работает из коробки. Вам нужно либо:
- Конвертировать его в
@themeCSS (рекомендуется для новой архитектуры) - Использовать слой совместимости — директиву
@config(быстрый путь миграции)
/* быстрая миграция — сохраняете старый конфиг */
@import "tailwindcss";
@config "../../tailwind.config.js";Этот мост @config работает, но он явно является инструментом миграции. Рекомендация — со временем перейти на @theme.
2. Удалённые устаревшие утилиты#
Некоторые утилиты, которые были помечены как устаревшие в v3, удалены:
/* УДАЛЕНЫ в v4 */
bg-opacity-* → используйте bg-black/50 (синтаксис со слэшем)
text-opacity-* → используйте text-black/50
border-opacity-* → используйте border-black/50
flex-shrink-* → используйте shrink-*
flex-grow-* → используйте grow-*
overflow-ellipsis → используйте text-ellipsis
decoration-slice → используйте box-decoration-slice
decoration-clone → используйте box-decoration-clone
Если вы уже использовали современный синтаксис в v3 (слэш для прозрачности, shrink-*), проблем не будет. Если нет — это простые изменения типа найти-и-заменить.
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. Изменения API плагинов#
Если вы писали кастомные плагины, API изменилось. Функции addUtilities, addComponents, addBase и addVariant по-прежнему работают через слой совместимости, но идиоматичный подход v4 — нативный CSS:
// плагин v3
plugin(function ({ addUtilities, theme }) {
addUtilities({
".scrollbar-hide": {
"-ms-overflow-style": "none",
"scrollbar-width": "none",
"&::-webkit-scrollbar": {
display: "none",
},
},
});
});/* v4 — просто 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 — часть >* теперь использует синтаксис inset-вариантов -->
<div class="*:p-4">...</div>Произвольный синтаксис вариантов [&...] по-прежнему работает, но v4 предоставляет именованные альтернативы для распространённых паттернов.
Руководство по миграции: реальный процесс#
Вот как я реально мигрировал — не счастливый путь из документации, а как процесс выглядел на самом деле.
Шаг 1: Запустите официальный codemod#
Tailwind предоставляет codemod, который обрабатывает большинство механических изменений:
npx @tailwindcss/upgradeОн автоматически делает многое:
- Конвертирует директивы
@tailwindв@import "tailwindcss" - Переименовывает устаревшие классы утилит
- Обновляет синтаксис вариантов
- Конвертирует утилиты прозрачности в синтаксис со слэшем (
bg-opacity-50вbg-black/50) - Создаёт базовый блок
@themeиз вашего конфига
Что codemod обрабатывает хорошо#
- Переименование классов утилит (почти идеально)
- Изменения синтаксиса директив
- Простые значения темы (цвета, отступы, шрифты)
- Миграция синтаксиса прозрачности
Что codemod НЕ обрабатывает#
- Сложные конвертации плагинов
- Динамические значения конфигурации (вызовы
theme()в JavaScript) - Условная конфигурация темы (например, значения темы в зависимости от окружения)
- Миграция 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 — до
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 — после */
@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 (если они у вас есть). Codemod хорош, но не идеален. Что я обнаружил при визуальной проверке:
- Несколько мест, где миграция синтаксиса прозрачности дала немного отличающиеся результаты
- Вывод кастомного плагина, который не перенёсся
- Изменения стекирования z-index из-за порядка слоёв
- Некоторые переопределения через
!important, которые вели себя по-другому с каскадными слоями
Шаг 5: Обновите сторонние зависимости#
Проверьте каждый пакет, связанный с Tailwind:
{
"@tailwindcss/typography": "^1.0.0",
"@tailwindcss/forms": "^1.0.0",
"@tailwindcss/container-queries": "УДАЛИТЬ — теперь встроено",
"tailwindcss-animate": "проверить поддержку v4",
"prettier-plugin-tailwindcss": "обновить до последней версии"
}Плагин @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 важнее из-за каскадных слоёв. Ваш @import "tailwindcss" должен идти до ваших кастомных стилей:
/* правильный порядок */
@import "tailwindcss";
@import "./theme.css";
@import "./custom-utilities.css";
/* ваши inline @theme, @utility и т.д. */Если вы импортируете кастомный CSS до Tailwind, ваши стили могут оказаться в каскадном слое с более низким приоритетом и неожиданно быть перекрыты.
Тёмная тема#
Тёмная тема работает так же концептуально, но конфигурация переместилась в CSS:
@import "tailwindcss";
/* Используем тёмную тему на основе класса (по умолчанию — на основе media query) */
@variant dark (&:where(.dark, .dark *));Это заменяет конфиг v3:
// v3
module.exports = {
darkMode: "class",
};Подход @variant более гибкий. Вы можете определить тёмную тему как угодно — на основе класса, data-атрибута или media query:
/* подход с data-атрибутом */
@variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));
/* media query — это значение по умолчанию, поэтому объявлять его не нужно */
@variant dark (@media (prefers-color-scheme: dark));Совместимость с Turbopack#
Если вы используете Next.js с Turbopack (который теперь является стандартным бандлером для разработки), 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: 34,2 КБ (gzipped)
вывод v4: 29,8 КБ (gzipped)
Снижение на 13% без изменения кода. Не революционно, но бесплатная производительность.
Tree shaking значений темы#
В v4, если вы определяете значение темы, но никогда не используете его в шаблонах, соответствующее CSS custom property всё равно будет сгенерировано (оно в @theme, который маппится на переменные :root). Однако утилитарные классы для неиспользуемых значений не генерируются. Это так же, как JIT-поведение v3, но стоит отметить: ваши CSS custom properties всегда доступны, даже для значений без использования утилит.
Если вы хотите предотвратить генерацию CSS custom properties для определённых значений темы, можно использовать @theme inline:
@theme inline {
/* Эти значения генерируют утилиты, но НЕ 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);
}
/* Переопределения бренда */
.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">
<!-- все bg-brand, text-brand и т.д. используют цвета Acme -->
<div class="bg-brand text-white">Acme Corp</div>
</body>В v3 для этого требовался кастомный плагин или сложная настройка CSS-переменных вне Tailwind. В v4 это естественно — тема это CSS-переменные, а CSS-переменные каскадируются. Именно это делает CSS-first подход правильным.
Чего мне не хватает из v3#
Позвольте быть объективным. Есть вещи, которые v3 делал, и которых мне действительно не хватает в v4:
1. JavaScript-конфиг для программных тем. У меня был проект, где мы генерировали шкалы цветов из одного brand-цвета с помощью JavaScript-функции в конфиге. В v4 этого нельзя сделать в @theme — вам нужен этап сборки, генерирующий CSS-файл, или нужно вычислить цвета один раз и вставить их. Слой совместимости @config помогает, но это не долгосрочная история.
2. IntelliSense на старте был лучше. Расширение для VS Code v3 имело годы полировки. 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
- Когда будете готовы: Мигрируйте продакшн-проекты во время спокойного спринта, с визуальным регрессионным тестированием
Миграция не болезненна, если вы к ней подготовитесь. Codemod обрабатывает 80% работы. Оставшиеся 20% — ручные, но простые. Выделите день для среднего проекта, два-три дня для крупного.
Tailwind v4 — это то, чем Tailwind должен был быть с самого начала. JavaScript-конфигурация всегда была уступкой инструментарию своего времени. CSS-first конфигурация, нативные каскадные слои, Rust-движок — это не тренды, это фреймворк, догоняющий платформу. Веб-платформа стала лучше, и Tailwind v4 опирается на неё, а не борется с ней.
Переход к написанию дизайн-токенов в CSS, их компоновке с помощью CSS-фич и делегированию каскадирования браузеру — это правильное направление. Потребовалось четыре мажорные версии, чтобы сюда добраться, но результат — самая цельная версия Tailwind на данный момент.
Начните свой следующий проект с ней. Вы не будете оглядываться назад.