Tailwind CSS — установка, utility-first и адаптивность

Tailwind CSS — CSS-фреймворк с подходом utility-first. Интерфейс собирается из небольших классов прямо в разметке: p-4 задаёт отступ, text-lg — размер текста, bg-blue-600 — фон, flex — Flexbox.

<button class="rounded-lg bg-blue-600 px-4 py-2 font-semibold text-white hover:bg-blue-700">
  Сохранить
</button>

Tailwind сканирует исходные файлы проекта, находит используемые классы и генерирует соответствующий CSS.

О версиях. В Tailwind CSS 4 используется CSS-first-настройка: @import "tailwindcss", @theme и автоматическое обнаружение исходников. tailwind.config.js и поле content характерны прежде всего для Tailwind CSS 3. Ниже рассмотрены оба подхода.

Содержание


Установка и настройка

Tailwind CSS 4 с Vite

npm install tailwindcss @tailwindcss/vite

vite.config.js:

import { defineConfig } from "vite";
import tailwindcss from "@tailwindcss/vite";

export default defineConfig({
  plugins: [tailwindcss()],
});

Основной CSS-файл:

@import "tailwindcss";

Подключение в JavaScript:

import "./styles.css";

Запуск:

npm run dev

Tailwind CSS 4 через CLI

npm install tailwindcss @tailwindcss/cli

src/input.css:

@import "tailwindcss";

Сборка с наблюдением:

npx @tailwindcss/cli -i ./src/input.css -o ./dist/styles.css --watch

Результат подключается обычным <link>:

<link rel="stylesheet" href="./dist/styles.css">

Tailwind CSS 4 через PostCSS

npm install tailwindcss @tailwindcss/postcss postcss

postcss.config.mjs:

export default {
  plugins: {
    "@tailwindcss/postcss": {},
  },
};

CSS-файл:

@import "tailwindcss";

Классическая настройка Tailwind CSS 3

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

tailwind.config.js:

/** @type {import('tailwindcss').Config} */
export default {
  content: [
    "./index.html",
    "./src/**/*.{html,js,ts,jsx,tsx,vue,svelte}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
};

Основной CSS-файл v3:

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

Для адаптивной страницы необходим viewport:

<meta name="viewport" content="width=device-width, initial-scale=1.0">

Utility-first подход

Вместо отдельного класса .card стили задаются комбинацией утилит:

<article class="max-w-md rounded-2xl bg-white p-6 shadow-lg">
  <h2 class="text-xl font-bold text-slate-900">Заголовок</h2>
  <p class="mt-2 leading-relaxed text-slate-600">Описание карточки.</p>
</article>

Преимущества:

Длинные наборы классов не обязательно переносить в CSS. Повторяемую разметку обычно выносят в компонент React, Vue, Svelte, шаблонизатора или серверного фреймворка.


Spacing: отступы и расстояния

Padding

<div class="p-4">Со всех сторон</div>
<div class="px-6 py-3">По горизонтали и вертикали</div>
<div class="pt-2 pr-4 pb-6 pl-8">Отдельно для каждой стороны</div>
Класс Назначение
p-* отступ со всех сторон
px-*, py-* горизонтальный и вертикальный отступ
pt-*, pr-*, pb-*, pl-* отдельная сторона

Margin

<div class="m-4">Внешний отступ</div>
<div class="mx-auto max-w-5xl">Центрирование контейнера</div>
<div class="mt-8 mb-4">Вертикальные отступы</div>
<div class="-mt-2">Отрицательный отступ</div>

gap и space-*

<div class="flex flex-wrap gap-4">...</div>
<div class="grid gap-x-6 gap-y-8">...</div>
<ul class="space-y-3">...</ul>

Для Grid и Flexbox, особенно с переносом строк, обычно удобнее gap. space-y-* и space-x-* добавляют расстояние между непосредственными соседями.

Размеры

<div class="w-full max-w-3xl">...</div>
<div class="min-h-screen">...</div>
<div class="size-12">Одинаковые width и height</div>

Произвольное значение указывается в квадратных скобках:

<div class="w-[42rem] max-w-[calc(100%-2rem)]">...</div>

Если нестандартное значение повторяется, лучше добавить именованный токен в тему.


Color: цветовые классы

<div class="bg-slate-100 text-slate-900 border-slate-300">...</div>
<button class="bg-blue-600 text-white hover:bg-blue-700">Кнопка</button>
<p class="text-red-600">Ошибка</p>
Префикс Назначение
text-* цвет текста
bg-* цвет фона
border-* цвет границы
outline-* цвет обводки
ring-* цвет декоративного кольца
fill-*, stroke-* оформление SVG

Прозрачность добавляется после /:

<div class="bg-black/50 border-white/20">...</div>

Произвольный цвет:

<div class="bg-[#1e40af] text-[rgb(255_255_255)]">...</div>

Typography: текстовые классы

<h1 class="text-4xl font-extrabold tracking-tight text-slate-950">
  Заголовок страницы
</h1>
<p class="mt-4 text-base leading-7 text-slate-600">
  Основной текст.
</p>
Группа Примеры
Размер text-sm, text-base, text-xl, text-4xl
Насыщенность font-normal, font-medium, font-semibold, font-bold
Высота строки leading-tight, leading-normal, leading-7
Интервал tracking-tight, tracking-wide
Выравнивание text-left, text-center, text-right
Семейство font-sans, font-serif, font-mono
Переполнение truncate, text-ellipsis, line-clamp-3

Адаптивный размер средствами CSS:

<h1 class="text-[clamp(2rem,5vw,4.5rem)] leading-none">...</h1>

Layout-классы

Flexbox:

<nav class="flex items-center justify-between gap-4">
  <a class="font-bold" href="#">Логотип</a>
  <div class="flex items-center gap-3">...</div>
</nav>

Полезные классы: flex, flex-col, flex-wrap, items-center, justify-between, flex-1, grow, shrink-0, basis-1/2.

Grid:

<section class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
  <article>...</article>
  <article>...</article>
  <article>...</article>
</section>

Автоматическая карточная сетка:

<section class="grid grid-cols-[repeat(auto-fit,minmax(min(100%,18rem),1fr))] gap-6">
  ...
</section>

В произвольных CSS-значениях пробелы заменяются подчёркиваниями.


Responsive-модификаторы

Tailwind следует mobile-first. Класс без префикса действует на всех размерах. sm:, md:, lg: и другие варианты начинают действовать с соответствующей минимальной ширины.

<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">...</div>
Вариант Минимальная ширина по умолчанию
sm: 40rem — 640 px при корневом размере 16 px
md: 48rem — 768 px
lg: 64rem — 1024 px
xl: 80rem — 1280 px
2xl: 96rem — 1536 px

sm: означает «от ширины sm», а не «только смартфон». Мобильные стили задаются без префикса:

<aside class="hidden md:block">...</aside>

Диапазон и произвольная точка:

<div class="md:max-lg:flex">Только в диапазоне md–lg</div>
<div class="max-md:hidden">Скрыто ниже md</div>
<div class="min-[900px]:grid-cols-3">...</div>

Состояния

<button
  class="bg-blue-600 px-4 py-2 text-white
         hover:bg-blue-700
         focus:outline-none focus-visible:ring-4 focus-visible:ring-blue-300
         active:translate-y-px
         disabled:cursor-not-allowed disabled:opacity-50"
>
  Сохранить
</button>

Частые варианты: hover:, focus:, focus-visible:, active:, visited:, disabled:, checked:, first:, last:, odd:, even:, dark:, motion-reduce:.

Варианты комбинируются:

<a class="text-blue-700 md:hover:text-blue-900 dark:text-blue-300">Ссылка</a>

group

Позволяет изменить потомка при состоянии родителя:

<a class="group block rounded-xl p-4 hover:bg-slate-100" href="#">
  <h2 class="font-semibold group-hover:text-blue-600">Документация</h2>
  <span class="inline-block group-hover:translate-x-1">Подробнее →</span>
</a>

peer

Позволяет учитывать состояние соседнего элемента:

<label class="block">
  <span>Email</span>
  <input class="peer mt-1 w-full rounded-md border p-2" type="email" required>
  <span class="hidden text-sm text-red-600 peer-invalid:block">
    Введите корректный адрес
  </span>
</label>

Элемент с peer-* размещается после элемента с классом peer.


Кастомизация темы в Tailwind CSS 4

В v4 дизайн-токены настраиваются через @theme:

@import "tailwindcss";

@theme {
  --color-brand-50: #eff6ff;
  --color-brand-600: #2563eb;
  --color-brand-700: #1d4ed8;
  --font-display: "Manrope", system-ui, sans-serif;
  --font-body: "Inter", system-ui, sans-serif;
  --breakpoint-xs: 30rem;
  --breakpoint-3xl: 120rem;
  --spacing-18: 4.5rem;
  --radius-card: 1.25rem;
}

Использование:

<section class="rounded-card bg-brand-50 p-18 xs:p-6">
  <h2 class="font-display text-brand-700">Заголовок</h2>
</section>

Пространства имён --color-*, --font-*, --breakpoint-*, --spacing-* и --radius-* создают связанные utility-классы или варианты. Эти значения одновременно являются обычными CSS custom properties.


Кастомизация tailwind.config.js

В Tailwind CSS 3 тему обычно расширяют через theme.extend:

/** @type {import('tailwindcss').Config} */
export default {
  content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx,vue,svelte}"],
  darkMode: "class",
  theme: {
    extend: {
      colors: {
        brand: {
          50: "#eff6ff",
          600: "#2563eb",
          700: "#1d4ed8",
        },
      },
      fontFamily: {
        display: ["Manrope", "system-ui", "sans-serif"],
        body: ["Inter", "system-ui", "sans-serif"],
      },
      screens: {
        xs: "480px",
        "3xl": "1920px",
      },
      spacing: {
        18: "4.5rem",
      },
      borderRadius: {
        card: "1.25rem",
      },
    },
  },
  plugins: [],
};

extend дополняет стандартную тему. Например, прямое определение theme.colors заменяет стандартную палитру, а theme.extend.colors сохраняет её и добавляет новые значения.

JavaScript-конфигурацию можно подключить в Tailwind CSS 4 для совместимости:

@config "../tailwind.config.js";
@import "tailwindcss";

Для нового проекта v4 предпочтительнее @theme, если его возможностей достаточно.


Компонентный подход

Повторяемый блок лучше вынести в компонент приложения:

export function Button({ children, variant = "primary", ...props }) {
  const variants = {
    primary: "bg-blue-600 text-white hover:bg-blue-700",
    secondary: "bg-slate-200 text-slate-900 hover:bg-slate-300",
  };

  return (
    <button
      className={`rounded-lg px-4 py-2 font-semibold ${variants[variant]}`}
      {...props}
    >
      {children}
    </button>
  );
}

Не собирайте имена классов из частей:

// Плохо: сканер может не увидеть итоговый класс
<div className={`bg-${color}-600`}>...</div>

Используйте полные статические строки:

const variants = {
  blue: "bg-blue-600 hover:bg-blue-700",
  red: "bg-red-600 hover:bg-red-700",
};

<div className={variants[color]}>...</div>

@apply

@apply переносит набор утилит в собственный CSS-класс:

.button {
  @apply inline-flex items-center justify-center rounded-lg px-4 py-2;
  @apply font-semibold transition-colors;
  @apply focus:outline-none focus-visible:ring-4;
}

.button--primary {
  @apply bg-blue-600 text-white hover:bg-blue-700;
  @apply focus-visible:ring-blue-300;
}
<button class="button button--primary">Сохранить</button>

@apply уместен для сторонней разметки, повторяемого публичного CSS API или постепенного внедрения Tailwind. Не стоит создавать отдельный класс для каждой одноразовой комбинации: это уменьшает пользу utility-first. В приложении повторение чаще устраняется компонентами.

Пользовательские компоненты можно разместить в слое:

@layer components {
  .prose-link {
    @apply font-medium text-blue-700 underline underline-offset-4;
    @apply hover:text-blue-900;
  }
}

Dark mode

Используйте вариант dark::

<article class="bg-white text-slate-900 dark:bg-slate-900 dark:text-slate-100">
  <h2 class="text-slate-950 dark:text-white">Заголовок</h2>
  <p class="text-slate-600 dark:text-slate-300">Описание</p>
</article>

В Tailwind CSS 4 по умолчанию вариант ориентируется на prefers-color-scheme.

Для ручного переключения классом .dark:

@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));
const root = document.documentElement;
const button = document.querySelector("[data-theme-toggle]");

button.addEventListener("click", () => {
  root.classList.toggle("dark");
  localStorage.setItem(
    "theme",
    root.classList.contains("dark") ? "dark" : "light"
  );
});

Восстановление настройки:

const saved = localStorage.getItem("theme");
const systemDark = matchMedia("(prefers-color-scheme: dark)").matches;

if (saved === "dark" || (!saved && systemDark)) {
  document.documentElement.classList.add("dark");
}

Код начального определения темы желательно выполнить в <head>, чтобы избежать вспышки неверной темы.

Для атрибута вместо класса:

@custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));

Оптимизация бандла

В старой терминологии удаление неиспользуемых классов называли purge. Tailwind CSS 3 сканирует пути из content:

export default {
  content: [
    "./index.html",
    "./src/**/*.{html,js,ts,jsx,tsx,vue,svelte}",
  ],
};

В Tailwind CSS 4 типичные исходники обнаруживаются автоматически. Tailwind рассматривает их как обычный текст: он не выполняет JavaScript и не вычисляет интерполяцию строк. Поэтому имена классов должны находиться в файлах целиком.

// Ненадёжно
`text-${size}`

// Надёжно
const sizes = {
  small: "text-sm",
  medium: "text-base",
  large: "text-lg",
};

Дополнительный источник в v4:

@import "tailwindcss";
@source "../shared-components";

Полностью явная настройка источников:

@import "tailwindcss" source(none);
@source "../src";
@source "../packages/ui";

Точечный safelist:

@source inline("bg-red-500 text-white hover:bg-red-700");

Практические правила:


Общий пример

<article
  class="group overflow-hidden rounded-2xl bg-white shadow-lg ring-1 ring-black/5
         transition hover:-translate-y-1 hover:shadow-xl
         dark:bg-slate-900 dark:ring-white/10"
>
  <img
    class="aspect-[4/3] w-full object-cover sm:aspect-video"
    src="product.jpg"
    alt="Описание товара"
  >

  <div class="p-4 sm:p-6">
    <span class="rounded-full bg-blue-50 px-2.5 py-1 text-xs font-semibold
                 text-blue-700 dark:bg-blue-950 dark:text-blue-300">
      Новинка
    </span>

    <h2 class="mt-3 text-xl font-bold tracking-tight text-slate-950
               group-hover:text-blue-700 sm:text-2xl
               dark:text-white dark:group-hover:text-blue-300">
      Название товара
    </h2>

    <p class="mt-2 line-clamp-3 leading-7 text-slate-600 dark:text-slate-300">
      Краткое описание товара с его главными преимуществами.
    </p>

    <button class="mt-6 w-full rounded-lg bg-blue-600 px-4 py-2.5 font-semibold
                   text-white hover:bg-blue-700 focus:outline-none
                   focus-visible:ring-4 focus-visible:ring-blue-300
                   disabled:cursor-not-allowed disabled:opacity-50"
            type="button">
      Добавить в корзину
    </button>
  </div>
</article>

Сетка карточек:

<section class="mx-auto grid w-[min(100%-2rem,80rem)]
                grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
  <!-- Карточки -->
</section>

Частые ошибки

sm: воспринимается как «мобильный стиль»

<!-- Мобильное значение должно быть без префикса -->
<div class="text-sm lg:text-xl">...</div>

Удаляется видимый фокус

<!-- Нужна доступная замена outline -->
<button class="focus:outline-none focus-visible:ring-4">...</button>

Слишком много произвольных значений

<div class="p-[17px] mt-[23px] rounded-[11px]">...</div>

Повторяющиеся значения следует перенести в тему.

Смешиваются инструкции v3 и v4

Для v4 типичны:

@import "tailwindcss";
@theme { ... }

Для v3 типичны @tailwind base, @tailwind components, @tailwind utilities и поле content в tailwind.config.js.


Краткая памятка

Отступы:       p-4 px-6 py-2 m-4 mx-auto mt-8 gap-4
Размеры:       w-full max-w-5xl min-h-screen size-12
Цвет:          bg-blue-600 text-white border-slate-300 bg-black/50
Текст:         text-lg font-semibold leading-7 tracking-tight
Flexbox:       flex flex-col items-center justify-between gap-4
Grid:          grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3
Оформление:    rounded-xl border shadow-lg ring-1
Responsive:    sm: md: lg: xl: 2xl: max-md: min-[900px]:
Состояния:     hover: focus: focus-visible: active: disabled:
Связи:         group group-hover: peer peer-invalid:
Тёмная тема:   dark:bg-slate-900 dark:text-white

Главный принцип: сначала соберите интерфейс из стандартных утилит, затем вынесите повторяемую разметку в компоненты и только после этого добавляйте токены, собственный CSS или @apply, когда для них есть практическая причина.

Официальная документация: установка, utility-классы, адаптивность, тема, dark mode, обнаружение классов.