Что такое вариант
Вариант — приставка перед утилитой. Она не меняет свойство, а добавляет условие, при котором свойство применяется:
.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">
<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>