CSS-first конфигурация, интеграция с @layer, встроенные container queries, производительность нового движка, ломающие изменения и мой честный опыт миграции с v3 на v4.
Я использую Tailwind CSS начиная с v1.x, ещё в те времена, когда половина сообщества считала его уродством, а другая половина не могла остановиться и продолжала выпускать продукты с ним. Каждая мажорная версия была значительным скачком, но v4 — это другое. Это не просто релиз с новыми функциями. Это архитектурное переписывание с нуля, которое меняет фундаментальный контракт между вами и фреймворком.
После миграции двух продакшн-проектов с v3 на v4 и запуска трёх новых проектов на v4 с чистого листа у меня сложилась ясная картина того, что действительно стало лучше, что ещё сыровато и стоит ли мигрировать прямо сейчас. Без хайпа, без возмущений — только то, что я наблюдал.
Tailwind CSS v4 — это три вещи одновременно:
tailwind.config.js в качестве варианта по умолчанию@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-паттерны.
Это самый большой концептуальный сдвиг. В 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-конфигурации».
В 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().
Движок 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 как об этапе сборки вообще. Он становится как подсветка синтаксиса — всегда рядом, никогда не мешает. Эта психологическая разница значительна за полный рабочий день разработки.
В 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 были одной из самых запрашиваемых функций в 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, миграция не тривиальна. Вот что сломалось в моих проектах:
Ваш tailwind.config.js не работает из коробки. Вам нужно либо:
@theme CSS (рекомендуется для новой архитектуры)@config (быстрый путь миграции)/* быстрая миграция — сохраняете старый конфиг */
@import "tailwindcss";
@config "../../tailwind.config.js";Этот мост @config работает, но он явно является инструментом миграции. Рекомендация — со временем перейти на @theme.
Некоторые утилиты, которые были помечены как устаревшие в 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-*), проблем не будет. Если нет — это простые изменения типа найти-и-заменить.
Палитра цветов по умолчанию немного сместилась. Если вы зависите от точных значений цветов из v3 (не по имени, а по конкретному hex-значению), вы можете заметить визуальные различия. Именованные цвета (blue-500, gray-200) по-прежнему существуют, но некоторые hex-значения изменились.
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";Если вы писали кастомные плагины, 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 версии. Сторонние плагины — как повезёт, проверяйте их репозиторий перед миграцией.
В v3 можно было отказаться от JIT-режима (хотя почти никто этого не делал). В v4 нет не-JIT-режима. Всё генерируется по запросу, всегда. Если у вас была какая-то причина использовать старый AOT-движок (ahead-of-time), этот путь больше не доступен.
Несколько вариантов переименованы или изменили поведение:
<!-- v3 -->
<div class="[&>*]:p-4">...</div>
<!-- v4 — часть >* теперь использует синтаксис inset-вариантов -->
<div class="*:p-4">...</div>Произвольный синтаксис вариантов [&...] по-прежнему работает, но v4 предоставляет именованные альтернативы для распространённых паттернов.
Вот как я реально мигрировал — не счастливый путь из документации, а как процесс выглядел на самом деле.
Tailwind предоставляет codemod, который обрабатывает большинство механических изменений:
npx @tailwindcss/upgradeОн автоматически делает многое:
@tailwind в @import "tailwindcss"bg-opacity-50 в bg-black/50)@theme из вашего конфигаtheme() в JavaScript)Для большинства конфигураций вы обновите ваш 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()],
});Это ручная часть. Возьмите значения темы из 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.
Это обязательно. После миграции я открыл каждую страницу моего приложения и визуально проверил её. Я также запустил скриншот-тесты Playwright (если они у вас есть). Codemod хорош, но не идеален. Что я обнаружил при визуальной проверке:
!important, которые вели себя по-другому с каскадными слоямиПроверьте каждый пакет, связанный с 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 под капотом, поэтому 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 в 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));Если вы используете 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-пайплайнов, где память ограничена, и для машин разработки, на которых одновременно работают десять процессов.
v4 генерирует немного меньший CSS на выходе, потому что новый движок лучше справляется с дедупликацией и устранением мёртвого кода:
вывод v3: 34,2 КБ (gzipped)
вывод v4: 29,8 КБ (gzipped)
Снижение на 13% без изменения кода. Не революционно, но бесплатная производительность.
В 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 делал, и которых мне действительно не хватает в 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:
shrink-* и т. д.)Для новых проектов v4 — очевидный выбор. CSS-first конфигурация чище, движок радикально быстрее, а новые функции (container queries, @starting-style, новые варианты) действительно полезны.
Для существующих проектов я рекомендую поэтапный подход:
Миграция не болезненна, если вы к ней подготовитесь. Codemod обрабатывает 80% работы. Оставшиеся 20% — ручные, но простые. Выделите день для среднего проекта, два-три дня для крупного.
Tailwind v4 — это то, чем Tailwind должен был быть с самого начала. JavaScript-конфигурация всегда была уступкой инструментарию своего времени. CSS-first конфигурация, нативные каскадные слои, Rust-движок — это не тренды, это фреймворк, догоняющий платформу. Веб-платформа стала лучше, и Tailwind v4 опирается на неё, а не борется с ней.
Переход к написанию дизайн-токенов в CSS, их компоновке с помощью CSS-фич и делегированию каскадирования браузеру — это правильное направление. Потребовалось четыре мажорные версии, чтобы сюда добраться, но результат — самая цельная версия Tailwind на данный момент.
Начните свой следующий проект с ней. Вы не будете оглядываться назад.