Разработка · 6 мин чтения

Tailwind CSS в Next.js: настройка и практические паттерны

Tailwind CSS в связке с Next.js - конфигурация, которую я ставлю в большинстве новых проектов: лендинги, админ-панели, SaaS-интерфейсы. За практику наберётся с десяток внедрений - от простого одностраничника до продакшен-дашборда с полутора сотней компонентов, и почти на каждом проекте всплывали одни и те же грабли: неправильные content-пути, конфликты между utility-классами и глобальными стилями, раздутый CSS-бандл из-за динамически собранных классов. Ниже - рабочая настройка и паттерны, которые снимают эти проблемы ещё на этапе архитектуры, а не разгребаются перед деплоем.

Установка Tailwind CSS в Next.js: App Router и версии 3 против 4

Если создаёте проект с нуля через create-next-app и отмечаете Tailwind на шаге установки, весь сетап делают за вас мастером. На практике чаще ставлю Tailwind руками поверх уже существующего Next.js-проекта - там своя структура папок, и автоматический конфиг под неё не подойдёт.

npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

В tailwind.config.js прописываю пути, где движок ищет классы:

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    './app/**/*.{js,ts,jsx,tsx,mdx}',
    './components/**/*.{js,ts,jsx,tsx,mdx}',
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}

В globals.css - три директивы, дальше подключаю файл в корневом layout:

@tailwind base;
@tailwind components;
@tailwind utilities;

С выходом Tailwind v4 часть конфигурации переехала в CSS: вместо tailwind.config.js с массивом content-путей движок сам сканирует файлы проекта, а тема задаётся блоком @theme прямо в CSS-файле:

@import "tailwindcss";

Для новых проектов беру v4 - сборка на движке Oxide заметно быстрее на больших кодовых базах, а часть плагинов (forms, typography) уже переписаны под новый формат. Для проектов, где завязаны кастомные плагины под старый API конфига, остаюсь на v3 - миграция ради миграции смысла не имеет.

Настройка content-путей и почему стили пропадают в проде

Самая частая жалоба клиентов на этапе деплоя: «в dev всё красиво, а на проде часть блоков без стилей». В 9 случаях из 10 причина в content-путях - в массиве не указана папка, откуда реально импортируются компоненты (например, src/app вместо app, или забытая папка features с общими блоками). В dev-режиме Tailwind пересобирает CSS по каждому изменению и часто маскирует проблему, потому что разработчик правит именно те файлы, что уже покрыты путями. На проде собирается финальный бандл строго по content-массиву, и всё, что мимо него, вылетает.

Вторая частая ошибка - динамически собранные названия классов через шаблонные строки:

const color = 'red'

<div className={`bg-${color}-500`}></div>
// класс не соберётся: Tailwind ищет по регуляркам в исходном тексте,
// а не выполняет JS, поэтому такой строки в файлах просто нет

Рабочий вариант - полный список классов в объекте-мапе, чтобы Tailwind увидел литеральную строку:

const colorMap = {
  red: 'bg-red-500',
  blue: 'bg-blue-500',
}

<div className={colorMap[color]}></div>

Если классы приходят с бэкенда или из CMS и заранее их не перечислить, добавляю safelist в конфиге - но использую его точечно, потому что каждый класс в safelist попадает в бандл независимо от того, применяется он реально или нет.

Бесплатный материал

🎁 Полезный скрипт в подарок

Подпишитесь на Telegram - пришлю готовый скрипт по этой теме.

Без спама. Отписка в 1 клик.

Как я организую классы: clsx, cva и переиспользуемые UI-компоненты

Без договорённостей className быстро превращается в строку на 200 символов с повторяющимися решениями по отступам, радиусам и цветам в каждом компоненте. На третьем-четвёртом лендинге для одного клиента это особенно заметно - правки дизайна расползаются по десятку файлов.

Выношу варианты компонентов через class-variance-authority (cva) - один источник правды на кнопку, бейдж или карточку:

import { cva } from 'class-variance-authority'

const button = cva('rounded-md font-medium transition-colors', {
  variants: {
    intent: {
      primary: 'bg-blue-600 text-white hover:bg-blue-700',
      ghost: 'bg-transparent text-gray-700 hover:bg-gray-100',
    },
    size: {
      sm: 'px-3 py-1.5 text-sm',
      md: 'px-4 py-2 text-base',
    },
  },
  defaultVariants: { intent: 'primary', size: 'md' },
})

Для условных и переопределяемых классов ставлю рядом clsx и tailwind-merge: clsx собирает классы по условиям, tailwind-merge убирает конфликты вроде одновременных px‑2 и px‑4, оставляя последний по порядку, а не оба сразу с непредсказуемым результатом из-за порядка подключения файлов.

Тёмная тема и дизайн-токены на CSS-переменных

дarkMode: ‘class’ в конфиге плюс next-themes для переключения атрибута на html - рабочая связка, которую ставлю почти везде, где есть переключатель темы или хотя бы системная тема по умолчанию. Вместо жёстко прописанных bg-white / dark:bg-gray-900 в каждом компоненте завожу переменные:

:root {
  --color-brand: 37 99 235;
  --color-bg: 255 255 255;
}

.dark {
  --color-bg: 17 24 39;
}

И подключаю их в theme.extend, используя формат rgb с каналами через пробел - так остаётся возможность менять прозрачность через модификатор /50 прямо в классе:

colors: {
  brand: 'rgb(var(--color-brand) / <alpha-value>)',
  surface: 'rgb(var(--color-bg) / <alpha-value>)',
}

Плюс такого подхода - смена бренд-цвета клиента правкой одной переменной вместо поиска-замены по полусотне файлов с bg-blue-600.

Оптимизация сборки: шрифты, arbitrary values и типичные ошибки

Шрифты подключаю через next/font, а не через Google Fonts CDN и отдельный @import в CSS - так шрифт грузится с того же домена без лишнего внешнего запроса и без сдвига layout при подгрузке:

import { Inter } from 'next/font/google'

const inter = Inter({ subsets: ['latin', 'cyrillic'] })

export default function RootLayout({ children }) {
  return (
    <html lang="ru" className={inter.className}>
      <body>{children}</body>
    </html>
  )
}

Отдельно слежу за произвольными значениями вроде w-[137px] или top-[42%] - иногда без них не обойтись, но если такие классы плодятся десятками с почти одинаковыми числами, это сигнал вынести значение в тему конфига как именованный токен. Каждое уникальное произвольное значение - отдельный класс в финальном CSS, и на проекте с полусотней таких вставок бандл ощутимо растёт без видимой пользы.

По ощущениям с реальных сборок, разница в весе финального CSS выглядит примерно так:

Сценарий Итоговый CSS (gzip)
Лендинг с аккуратными content-путями и cva-компонентами 8-12 КБ
Тот же лендинг с десятками arbitrary values и safelist «на всякий случай» 25-35 КБ
Проект без чистки неиспользуемых плагинов и с ручным подключением всех дефолтных цветов Tailwind 40+ КБ

Ещё одна частая ошибка - злоупотребление @apply в CSS-файлах вместо утилит прямо в разметке: это возвращает специфичность обычного CSS и конфликты порядка подключения, ради которых Tailwind изначально и обходили утилитарным подходом. Использую @apply точечно - для переиспользуемых базовых стилей типа .prose, но не для компонентных классов.

Практические паттерны компонентов: сетки, формы и sticky-навигация

Шапка с эффектом размытия при скролле - паттерн, который прошу почти на каждом лендинге:

<header class="sticky top-0 z-50 backdrop-blur bg-white/80 border-b border-gray-200">
  ...
</header>

<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
  ...
</div>

Для форм ставлю @tailwindcss/forms - он сбрасывает браузерные стили инпутов и чекбоксов к единому виду, и дальше накидываю утилиты поверх без борьбы с дефолтным видом select в разных браузерах:

npm install -D @tailwindcss/forms

Часть таких секций - сетка тарифов, sticky-шапка, форма заявки с валидацией - я собираю один раз и переиспользую как базу для новых лендингов вместо того, чтобы каждый раз верстать с нуля; готовые заготовки собраны в библиотеке готовых скриптов.

Быстрый SEO-лендинг под продукт

Лендинг на Next.js

от 80 000 ₽

Подробнее →

Частые вопросы

Tailwind CSS замедляет сборку Next.js?

Нет, скорее наоборот. JIT-движок компилирует только реально используемые классы и делает это на этапе сборки, а не в рантайме браузера, как в случае с CSS-in-JS решениями. На проектах среднего размера пересборка при изменении одного файла в dev укладывается в доли секунды, а сама сборка на движке v4 быстрее аналогичной на v3 в разы на больших кодовых базах.

Можно ли совмещать Tailwind с CSS-модулями в Next.js?

Можно, и иногда это разумный выбор - например, для сложных анимаций с кастомными keyframes или редких селекторов, которые неудобно выражать утилитами. Держу CSS-модули как исключение, а не параллельную систему стилей: если дублировать дизайн-токены (отступы, цвета) в обоих подходах, они быстро расходятся между собой.

Какую версию Tailwind выбрать для нового проекта на Next.js - 3 или 4?

Для нового проекта беру v4: CSS-first конфигурация проще для новых участников команды, а сборка быстрее. Остаюсь на v3, если проект завязан на плагины со старым форматом API конфига или устаревшие сборки CI, где обновление потянет за собой отдельную задачу на миграцию плагинов.

Сколько стоит адаптировать существующий Next.js-проект под Tailwind?

Зависит от объёма вёрстки и того, есть ли в проекте уже конфликтующая система стилей вроде styled-components. Оценку по конкретному репозиторию даю на консультации от 3 000 ₽, а полноценный лендинг на Next.js с нуля с учётом всех паттернов из статьи - от 80 000 ₽.

Есть задача?

Обсудим в мессенджере

Расскажите, что нужно сделать — отвечу в течение 4 часов в рабочее время. Первая консультация бесплатно.

Самозанятый Калинкин Н. А. · работаю с физлицами и юрлицами

Продолжая пользование настоящим сайтом Вы выражаете своё согласие на обработку Ваших персональных данных (файлов куки) с использованием Yandex.Metrika.
Понятно