Урок 12. Теория: как работают варианты

📁 Раздел: Состояния и темы ⏱️ Время изучения: ~50 мин 🎯 Сложность: Средняя

⚡ Кратко

Вариант превращает утилиту в правило с условием: hover:bg-slate-100 — это .hover\:bg-slate-100:hover. Варианты комбинируются (md:hover:focus-visible:), читают состояние родителя (group-*), соседа (peer-*) и содержимого (has-*). Благодаря этому большая часть интерактивности обходится без JavaScript.

Что такое вариант

Вариант — приставка перед утилитой. Она не меняет свойство, а добавляет условие, при котором свойство применяется:

.hover\:bg-slate-100:hover { background-color: … }
.md\:flex { @media (width >= 48rem) { display: flex } }
.disabled\:opacity-50:disabled { opacity: 0.5 }

Условия складываются слева направо. md:hover:bg-white читается как «на экранах от 768px, при наведении — белый фон».

Базовые состояния

<div class="flex flex-wrap items-center gap-3">
  <button class="rounded-md bg-sky-600 px-4 py-2 text-sm font-medium text-white hover:bg-sky-700 active:bg-sky-800
                 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-600">
    Наведите и нажмите
  </button>
  <button class="rounded-md bg-slate-200 px-4 py-2 text-sm font-medium text-slate-500 disabled:cursor-not-allowed disabled:opacity-60" disabled>
    Отключена
  </button>
</div>
ВариантКогда срабатывает
hover:курсор над элементом
focus:элемент в фокусе (мышью или клавиатурой)
focus-visible:фокус получен с клавиатуры
focus-within:фокус внутри элемента
active:кнопка нажата
disabled:элемент отключён
checked:чекбокс или радио отмечены
Почему focus-visible, а не focus: обычный focus срабатывает и при клике мышью — кнопка остаётся обведённой после нажатия, что выглядит как дефект. focus-visible показывает контур только тем, кто ходит по интерфейсу с клавиатуры.

Варианты — это условия в рецепте: «если тесто поднялось — ставим в духовку». Утилита говорит, что делать, вариант — когда. Именно поэтому один и тот же bg-slate-100 может быть и обычным фоном, и фоном только при наведении, и фоном только в тёмной теме.

group: реакция на родителя

Пометьте родителя классом group, и дети смогут реагировать на его состояние через group-hover:, group-focus:, group-has-[…]:.

<a href="#" class="group flex max-w-sm items-center gap-3 rounded-xl border border-slate-200 bg-white p-4 hover:border-sky-400 hover:bg-sky-50">
  <div class="size-10 shrink-0 rounded-lg bg-slate-100 transition-colors group-hover:bg-sky-100"></div>
  <div class="min-w-0">
    <p class="text-sm font-semibold text-slate-900 group-hover:text-sky-700">Наведите на карточку</p>
    <p class="text-xs text-slate-500">Меняются сразу три вложенных элемента</p>
  </div>
  <span class="ms-auto text-slate-300 transition-transform group-hover:translate-x-1 group-hover:text-sky-500">→</span>
</a>

Без group пришлось бы писать CSS-правило .card:hover .icon { … } — то есть уходить от утилитарного подхода.

peer: реакция на соседа

peer ставится на элемент, за состоянием которого следим, а peer-* — на следующего соседа. Классика — свой переключатель на базе настоящего чекбокса:

<label class="inline-flex cursor-pointer items-center gap-3">
  <input type="checkbox" class="peer sr-only">
  <span class="relative h-6 w-11 rounded-full bg-slate-300 transition-colors peer-checked:bg-emerald-500 peer-focus-visible:ring-2 peer-focus-visible:ring-emerald-500/40">
    <span class="absolute top-0.5 left-0.5 size-5 rounded-full bg-white transition-transform peer-checked:translate-x-5"></span>
  </span>
  <span class="text-sm text-slate-700 peer-checked:text-emerald-700">Уведомления включены</span>
</label>
Порядок в разметке важен: peer должен идти раньше зависимого элемента. CSS умеет смотреть только вперёд — на следующих соседей.

has: реакция на содержимое

has-[…] позволяет стилизовать родителя по тому, что внутри. Это то, ради чего раньше писали JavaScript:

<div class="grid gap-3 sm:grid-cols-2">
  <label class="flex cursor-pointer items-center gap-3 rounded-lg border border-slate-200 p-3 has-checked:border-sky-500 has-checked:bg-sky-50">
    <input type="radio" name="plan-demo" class="accent-sky-600">
    <span class="text-sm text-slate-800">Обычный тариф</span>
  </label>
  <label class="flex cursor-pointer items-center gap-3 rounded-lg border border-slate-200 p-3 has-checked:border-sky-500 has-checked:bg-sky-50">
    <input type="radio" name="plan-demo" class="accent-sky-600" checked>
    <span class="text-sm text-slate-800">Расширенный тариф</span>
  </label>
</div>

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

Состояния по атрибутам

Компоненты вроде меню и диалогов хранят состояние в атрибутах. Tailwind умеет читать их напрямую:

<button aria-expanded="true" class="aria-expanded:bg-slate-100">Меню</button>
<div data-state="open" class="data-[state=open]:rotate-180">▾</div>
<input aria-invalid="true" class="aria-invalid:border-rose-500">
data-state="open" data-state="closed"
<div class="flex flex-wrap items-center gap-4">
  <span data-state="open" class="rounded-md bg-slate-100 px-3 py-2 text-sm text-slate-700 data-[state=open]:bg-emerald-100 data-[state=open]:text-emerald-800">data-state="open"</span>
  <span data-state="closed" class="rounded-md bg-slate-100 px-3 py-2 text-sm text-slate-700 data-[state=open]:bg-emerald-100">data-state="closed"</span>
  <input aria-invalid="true" value="Некорректная почта" class="rounded-md border border-slate-300 px-3 py-2 text-sm aria-invalid:border-rose-500 aria-invalid:bg-rose-50">
</div>

Именно на этом держится оформление компонентов shadcn/ui: библиотека проставляет data-state, а стили описываются вариантами (уроки 18 и 21).

Позиция в списке

  • Первая строка
  • Вторая строка
  • Третья строка
  • Четвёртая строка
<ul class="max-w-sm overflow-hidden rounded-lg border border-slate-200 text-sm">
  <li class="border-b border-slate-200 px-4 py-2 odd:bg-slate-50 last:border-b-0">Первая строка</li>
  <li class="border-b border-slate-200 px-4 py-2 odd:bg-slate-50 last:border-b-0">Вторая строка</li>
  <li class="border-b border-slate-200 px-4 py-2 odd:bg-slate-50 last:border-b-0">Третья строка</li>
  <li class="border-b border-slate-200 px-4 py-2 odd:bg-slate-50 last:border-b-0">Четвёртая строка</li>
</ul>

Полезные варианты: first:, last:, odd:, even:, nth-3:, empty: (пустой элемент), only: (единственный ребёнок).

Комбинирование и отрицание

  • md:hover:bg-white — несколько условий сразу;
  • not-hover:opacity-80 — когда наведения нет;
  • group-hover:peer-checked:flex — сочетание зависимостей;
  • *:mt-2 — применить ко всем прямым детям;
  • **:text-sm — ко всем потомкам.

Каждому прямому ребёнку — одинаковое оформление

без повторения классов

через вариант *:

<div class="max-w-sm rounded-lg border border-slate-200 p-3 *:rounded-md *:bg-slate-100 *:px-3 *:py-2 *:text-sm space-y-2">
  <p>Каждому прямому ребёнку — одинаковое оформление</p>
  <p>без повторения классов</p>
  <p>через вариант *:</p>
</div>