Варіанти: hover, focus, responsive, dark mode та нові v4
Варіанти: hover, focus, responsive, dark mode та нові v4
Що таке варіант у Tailwind?
Tailwind-клас можна уявити як рівняння:
{варіант}:{утиліта} = {умова, за якої CSS застосовується} + {саме CSS-правило}
hover:bg-blue-500 — це не просто колір. Це умова (елемент у стані hover) + правило (background: blue-500).
Без варіантів у вас один стан. З варіантами — ваш елемент реагує на взаємодію, розмір екрану, тему, DOM-структуру. Один клас — одна умова. Кілька варіантів можна стекати: dark:hover:bg-blue-700 — при dark режимі і hover одночасно.
Ваш HTML-атрибут class стає декларативним описом поведінки елемента.
Варіанти взаємодії
hover, focus, active
<!-- Hover: зміна при наведенні мишею -->
<button class="bg-indigo-500 hover:bg-indigo-600">Наведить мишу</button>
<!-- Focus: зміна при фокусі (Tab або клік) -->
<input class="border-gray-300 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500/20" />
<!-- Active: зміна під час кліку (натискання) -->
<button class="bg-indigo-500 active:scale-95 active:bg-indigo-700">Натисніть</button>
<!-- Комбінація: hover + active + focus — повна інтерактивна кнопка -->
<button
class="
bg-indigo-600
hover:bg-indigo-700
active:bg-indigo-800 active:scale-95
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2
transition-all
"
>
Кнопка
</button>
focus vs focus-visible: focus спрацьовує завжди при фокусі (включно з кліком мишею). focus-visible — тільки при навігації клавіатурою. Для кнопок переважно focus-visible — рамка не з'являється при кліку, але допомагає клавіатурним користувачам.
visited, disabled, checked, required, invalid
<!-- Посилання — відвідані мають інший колір -->
<a class="text-blue-600 visited:text-purple-600">Посилання</a>
<!-- Вимкнений елемент -->
<button class="bg-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed" disabled>Вимкнено</button>
<!-- Checkbox — різний вигляд при checked -->
<input type="checkbox" class="border-gray-300 checked:border-indigo-500 checked:bg-indigo-500" />
<!-- Обов'язкове поле -->
<input class="border-gray-300 required:border-red-400" required />
<!-- Валідація -->
<input class="border-gray-300 invalid:border-red-500 invalid:ring-red-500/20 focus:ring-2" type="email" />
placeholder, file, selection
<!-- Placeholder (підказка в пустому input) -->
<input class="placeholder:text-gray-400 placeholder:italic" placeholder="Введіть текст..." />
<!-- file input — стилізація кнопки вибору файлу -->
<input
type="file"
class="file:mr-3 file:py-2 file:px-4 file:rounded-lg file:border-0
file:font-semibold file:bg-indigo-50 file:text-indigo-700
hover:file:bg-indigo-100"
/>
<!-- selection — виділений мишею текст -->
<p class="selection:bg-indigo-500 selection:text-white">Виділіть цей текст — побачите кольорове виділення</p>
Структурні псевдокласи
first, last, odd, even
<ul class="divide-y divide-gray-200">
<!-- Перший: без верхнього border -->
<!-- Парні: інший фон -->
<li class="py-3 px-4 first:rounded-t-xl last:rounded-b-xl odd:bg-white even:bg-gray-50">Пункт списку</li>
</ul>
nth-child з фільтром (НОВА v4)
У Tailwind v4 :nth-child отримав підтримку фільтра по класу:
<!-- Кожен 2-й елемент -->
<div class="nth-2:bg-gray-100">...</div>
<!-- Кожен 3-й -->
<div class="nth-3:opacity-50">...</div>
<!-- Складний вираз -->
<div class="nth-[2n+1]:bg-indigo-50">...</div>
<!-- З фільтром класу: кожен 2-й .post, ігноруючи інші -->
<div class="nth-[2n_of_.post]:bg-slate-50">...</div>
only, empty, first-of-type, last-of-type
<!-- Відображається тільки якщо є єдиним дочірнім -->
<div class="only:block hidden">Я єдиний</div>
<!-- Порожній елемент — приховати або стилізувати -->
<div class="empty:hidden">...</div>
<!-- Перший рядок таблиці певного типу -->
<tr class="first-of-type:bg-gray-50">
...
</tr>
Псевдоелементи
before та after
<!-- Декоративний елемент до тексту -->
<span class="before:content-['→'] before:mr-2 before:text-indigo-500"> Пункт зі стрілкою </span>
<!-- Бейдж з лічильником (через data-атрибут) -->
<button class="relative before:content-[attr(data-count)] before:absolute before:top-0 before:right-0" data-count="3">
Повідомлення
</button>
<!-- Зірочка для обов'язкових полів -->
<label class="after:content-['*'] after:text-red-500 after:ml-1"> Email </label>
marker, first-line, first-letter, backdrop
<!-- Маркер списку -->
<ul class="marker:text-indigo-500 marker:text-lg">
<li>Пункт 1</li>
<li>Пункт 2</li>
</ul>
<!-- Перший рядок параграфу -->
<p class="first-line:uppercase first-line:tracking-widest first-line:text-indigo-600">
Перший рядок буде великими літерами...
</p>
<!-- Перша літера — ефект капітелі -->
<p class="first-letter:text-4xl first-letter:font-black first-letter:float-left first-letter:mr-2">
Давним давно у далекій галактиці...
</p>
<!-- Backdrop для dialog/modal -->
<dialog class="backdrop:bg-black/50 backdrop:backdrop-blur-sm">Діалог із затемненим фоном</dialog>
Group та Peer: реакція на стан сусідніх/батьківських елементів
group — батьківський hover на дочірні
Коли потрібно, щоб дочірній елемент реагував на hover батька:
<!-- group на батьку, group-hover: на дочірніх -->
<div class="group p-4 bg-white rounded-xl border hover:border-indigo-300 hover:bg-indigo-50 transition-colors">
<h3 class="font-bold text-slate-800 group-hover:text-indigo-700 transition-colors">Заголовок</h3>
<p class="text-slate-500 text-sm group-hover:text-indigo-600/70 transition-colors">
При наведенні на картку — весь текст змінює колір
</p>
<span class="opacity-0 group-hover:opacity-100 transition-opacity text-indigo-500 text-sm font-semibold">
Читати →
</span>
</div>
group/name — іменовані групи (для вкладень)
<!-- Проблема без іменування: group-hover реагує на будь-який group-батько -->
<!-- Рішення: ім'я групи через group/{name} -->
<div class="group/card ...">
<div class="group/btn ...">
<!-- Реагує тільки на hover .group/btn, а не .group/card -->
<span class="group-hover/btn:text-indigo-500">...</span>
</div>
<!-- Реагує тільки на hover .group/card -->
<span class="group-hover/card:opacity-100">...</span>
</div>
peer — реакція на стан сусіднього елемента
Коли потрібно, щоб наступний сусідній елемент реагував на стан попереднього:
<!-- peer на input, peer-checked: на label -->
<input id="toggle" type="checkbox" class="peer sr-only" />
<label
for="toggle"
class="flex items-center gap-2 cursor-pointer text-sm font-medium text-slate-700
peer-checked:text-indigo-600"
>
<!-- Перемикач: peer-checked змінює вигляд -->
<div class="w-9 h-5 bg-gray-300 peer-checked:bg-indigo-500 rounded-full transition-colors relative">
<div
class="absolute top-0.5 left-0.5 size-4 bg-white rounded-full shadow transition-transform peer-checked:translate-x-4"
></div>
</div>
Увімкнути сповіщення
</label>
Важливо: peer і peer-*: мають бути на сусідніх елементах. Peer-елемент йде перед елементом, що реагує (у DOM-порядку).
<div class="p-6 space-y-6 bg-white rounded-2xl border border-slate-200" style="font-family: system-ui, sans-serif;">
<!-- Group demo -->
<div>
<p class="text-xs font-bold text-slate-400 uppercase tracking-widest mb-3">group-hover</p>
<div class="grid grid-cols-2 gap-3">
<div
class="group p-4 bg-white rounded-xl border border-slate-200 hover:border-indigo-300 hover:bg-indigo-50 transition-all cursor-pointer"
>
<div
class="size-8 bg-indigo-100 group-hover:bg-indigo-500 rounded-lg flex items-center justify-center text-lg transition-colors mb-2"
>
🎨
</div>
<h3 class="font-bold text-slate-800 group-hover:text-indigo-700 text-sm transition-colors">Дизайн</h3>
<p class="text-xs text-slate-500 group-hover:text-indigo-500/70 mt-1 transition-colors">
Компоненти та UI
</p>
</div>
<div
class="group p-4 bg-white rounded-xl border border-slate-200 hover:border-emerald-300 hover:bg-emerald-50 transition-all cursor-pointer"
>
<div
class="size-8 bg-emerald-100 group-hover:bg-emerald-500 rounded-lg flex items-center justify-center text-lg transition-colors mb-2"
>
⚡
</div>
<h3 class="font-bold text-slate-800 group-hover:text-emerald-700 text-sm transition-colors">
Продуктивність
</h3>
<p class="text-xs text-slate-500 group-hover:text-emerald-500/70 mt-1 transition-colors">Оптимізація</p>
</div>
</div>
</div>
<!-- Peer demo: Toggle switch -->
<div>
<p class="text-xs font-bold text-slate-400 uppercase tracking-widest mb-3">peer-checked</p>
<div class="space-y-3">
<label
class="flex items-center justify-between p-3 border border-slate-200 rounded-xl cursor-pointer hover:border-indigo-200 transition-colors"
>
<div>
<p class="text-sm font-semibold text-slate-700">Email сповіщення</p>
<p class="text-xs text-slate-500">Отримувати листи про новини</p>
</div>
<div class="relative flex-shrink-0 ml-4">
<input type="checkbox" class="sr-only peer" checked />
<div class="w-10 h-5 bg-slate-300 peer-checked:bg-indigo-500 rounded-full transition-colors"></div>
<div
class="absolute top-0.5 left-0.5 size-4 bg-white rounded-full shadow transition-transform peer-checked:translate-x-5"
></div>
</div>
</label>
<label
class="flex items-center justify-between p-3 border border-slate-200 rounded-xl cursor-pointer hover:border-indigo-200 transition-colors"
>
<div>
<p class="text-sm font-semibold text-slate-700">Push сповіщення</p>
<p class="text-xs text-slate-500">Сповіщення у браузері</p>
</div>
<div class="relative flex-shrink-0 ml-4">
<input type="checkbox" class="sr-only peer" />
<div class="w-10 h-5 bg-slate-300 peer-checked:bg-indigo-500 rounded-full transition-colors"></div>
<div
class="absolute top-0.5 left-0.5 size-4 bg-white rounded-full shadow transition-transform peer-checked:translate-x-5"
></div>
</div>
</label>
</div>
</div>
</div>
has-* варіант — батьківський селектор (НОВА v4)
has-* — це :has() CSS-псевдоклас у вигляді Tailwind-варіанту:
<!-- Картка змінює layout якщо має зображення -->
<div class="flex flex-col has-[img]:flex-row gap-4 p-4 bg-white rounded-xl border">
<img src="..." class="w-32 h-24 object-cover rounded-lg" />
<div>
<h3>Заголовок</h3>
<p>Опис</p>
</div>
</div>
<!-- Label підсвічується при фокусі вкладеного input — без JS! -->
<label class="block text-sm font-medium text-slate-700 has-[:focus]:text-indigo-600 transition-colors">
Email
<input type="email" class="mt-1 block w-full border border-gray-300 rounded-lg px-3 py-2" />
</label>
<!-- Форма показує кнопку тільки якщо є заповнений input -->
<form class="has-[input:not(:placeholder-shown)]:border-indigo-300 border rounded-xl p-4 transition-colors">
<input type="text" placeholder="Почніть вводити..." class="w-full focus:outline-none" />
</form>
not-* варіант (НОВА v4)
not-* застосовує стилі коли умова НЕ виконується:
<!-- Всі елементи крім першого мають border-top -->
<li class="not-first:border-t border-gray-200 py-3">...</li>
<!-- Всі крім disabled -->
<button class="bg-indigo-500 not-disabled:hover:bg-indigo-600 disabled:opacity-50">...</button>
<!-- Елементи що не мають класу .featured -->
<div class="not-[.featured]:opacity-70">...</div>
in-* варіант (НОВА v4)
in-* застосовує стилі коли елемент знаходиться всередині певного контейнера:
<!-- Посилання всередині .prose мають інший стиль -->
<a class="text-indigo-600 in-[.prose]:text-slate-700 in-[.prose]:underline"> Посилання </a>
<!-- Кнопка в nav vs в card виглядає по-різному -->
<button class="font-semibold in-[nav]:text-sm in-[.card]:text-base">Кнопка</button>
starting: варіант — аніматця при першій появі (НОВА v4)
starting: відповідає CSS @starting-style — стан елемента при першій появі в DOM:
<!-- Елемент появляється з прозорого і трансформує -->
<div
class="opacity-100 translate-y-0 transition-all duration-300
starting:opacity-0 starting:translate-y-2"
>
Цей блок анімується при появі
</div>
<!-- Dialog з анімацією появи -->
<dialog
class="opacity-100 scale-100 transition-all duration-200
starting:opacity-0 starting:scale-95"
>
<p>Зміст діалогу</p>
</dialog>
Адаптивні варіанти (Responsive)
Mobile-First підхід
Tailwind використовує mobile-first порядок. Клас без префіксу → для всіх розмірів. Breakpoint-варіант → від цього розміру і більше:
<!-- Мобільний: 1 колонка, sm+: 2, lg+: 4 -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<!-- Текст: мобільний small, lg+ великий -->
<h1 class="text-2xl lg:text-5xl font-black">
<!-- Flex-col на мобільному, flex-row на md+ -->
<div class="flex flex-col md:flex-row items-start md:items-center gap-4"></div>
</h1>
</div>
max-* варіанти (НОВА v4) — тільки до breakpoint
<!-- Приховано тільки на мобільному (до sm) -->
<div class="max-sm:hidden">Видно тільки на sm+</div>
<!-- Видно тільки між md і lg -->
<div class="max-lg:hidden md:block lg:hidden">Тільки на md</div>
<!-- padding тільки до md -->
<div class="max-md:px-4">На md+ — нема горизонтального padding</div>
Dark Mode варіант
Автоматичний через OS
За замовчуванням dark: відповідає CSS @media (prefers-color-scheme: dark):
<div class="bg-white dark:bg-slate-900 text-slate-900 dark:text-white">
<h1 class="text-slate-800 dark:text-slate-100">Заголовок</h1>
<p class="text-slate-600 dark:text-slate-400">Текст</p>
</div>
Ручне через CSS selector
/* main.css */
@import 'tailwindcss';
/* Tailwind застосовує dark: при наявності .dark на батьківському */
@custom-variant dark (&:is(.dark *));
<html class="dark">
<!-- Додати/видалити .dark через JS -->
<body>
<div class="bg-white dark:bg-slate-900">...</div>
</body>
</html>
// Перемикач теми
const toggle = () => document.documentElement.classList.toggle('dark')
Рекомендований підхід: CSS Custom Properties + dark mode у @layer
Замість тисячі dark: класів — краще semantic tokens:
@layer base {
:root {
--bg: oklch(1 0 0); /* white */
--surface: oklch(0.97 0 0); /* gray-50 */
--text: oklch(0.15 0.02 270);
--muted: oklch(0.55 0.02 270);
--border: oklch(0.9 0.01 270);
}
.dark {
--bg: oklch(0.13 0.03 270);
--surface: oklch(0.19 0.03 270);
--text: oklch(0.95 0.01 270);
--muted: oklch(0.65 0.02 270);
--border: oklch(0.28 0.02 270);
}
}
<!-- Без dark: скрізь — CSS-змінні автоматично -->
<div class="bg-[--bg] text-[--text] border border-[--border]"></div>
Стекання варіантів
Варіанти можна комбінувати в будь-якому порядку:
<!-- dark + hover: -->
<button class="bg-indigo-500 dark:bg-indigo-400 hover:bg-indigo-600 dark:hover:bg-indigo-300">
<!-- md + dark + hover: -->
<div class="md:dark:hover:bg-slate-700">
<!-- group-hover + dark -->
<div class="opacity-0 group-hover:opacity-100 dark:group-hover:opacity-80">
<!-- focus-visible + dark -->
<input class="focus-visible:ring-2 focus-visible:ring-indigo-500 dark:focus-visible:ring-indigo-400" />
</div>
</div>
</button>
Довільні варіанти
Для нестандартних умов — довільний варіант у квадратних дужках:
<!-- nth-child без утиліти -->
<li class="[&:nth-child(3)]:font-bold">...</li>
<!-- Вибрати дочірній p -->
<div class="[&_p]:text-slate-600 [&_p]:leading-relaxed">
<p>Цей рядок отримає стилі через довільний варіант</p>
<p>І цей теж</p>
</div>
<!-- Лише для touch-пристроїв -->
<div class="[@media(hover:hover)]:hover:opacity-75">Hover тільки там, де він дійсно є (не touch)</div>
<!-- Підтримка певної CSS-можливості -->
<div class="[@supports(display:grid)]:grid">
<!-- Специфічний data-атрибут -->
<div class="data-[state=open]:rotate-180 transition-transform">Іконка стрілки розкриває при data-state="open"</div>
</div>
Шпаргалка: всі варіанти в одному місці
hover: — при наведенні
focus: — при фокусі (будь-яким способом)
focus-within: — при фокусі всередині контейнера
focus-visible: — при фокусі через клавіатуру (accessibility)
active: — під час кліку
visited: — відвідане посилання
disabled: — disabled атрибут
enabled: — не disabled
checked: — checked checkbox/radio
indeterminate: — напівперевіркований checkbox
required: — required атрибут
optional: — без required
valid: — валідний input
invalid: — невалідний input
in-range: — число у межах min/max
out-of-range: — число поза межами
placeholder-shown: — input з placeholder (порожній)
autofill: — браузерний автофіл
read-only: — readonly атрибут
first: — перший дочірній
last: — останній дочірній
only: — єдиний дочірній
odd: — непарний (1-й, 3-й, 5-й...)
even: — парний (2-й, 4-й, 6-й...)
nth-{n}: — конкретний nth-child (НОВА v4)
nth-[expr]: — nth-child з виразом (НОВА v4)
first-of-type: — перший дочірній свого типу
last-of-type: — останній свого типу
only-of-type: — єдиний свого типу
empty: — без дочірніх
before: — ::before
after: — ::after
placeholder: — ::placeholder
file: — ::file-selector-button
marker: — ::marker
selection: — ::selection
first-line: — ::first-line
first-letter: — ::first-letter
backdrop: — ::backdrop (для dialog)
group: — позначити батька
group-{variant}: — реагувати на стан group/{variant}
group/{name}: — іменована група
group-{variant}/{name}: — іменований варіант групи
peer: — позначити попередній сусідній
peer-{variant}: — реагувати на стан peer/{variant}
peer/{name}: — іменований peer
has-[selector]: — батько містить selector (НОВА v4)
in-[selector]: — елемент всередині selector (НОВА v4)
not-{variant}: — заперечення (НОВА v4)
sm: — min-width: 640px
md: — min-width: 768px
lg: — min-width: 1024px
xl: — min-width: 1280px
2xl: — min-width: 1536px
max-sm: — max-width: 639px (НОВА v4)
max-md: — max-width: 767px (НОВА v4)
max-lg: — max-width: 1023px (НОВА v4)
dark: — темна тема
print: — стилі для друку
portrait: — портретна орієнтація
landscape: — ландшафтна орієнтація
starting: — @starting-style (анімація при появі)
nth-{n}: — :nth-child() з фільтром по класу
has-[...]: — :has() псевдоклас
not-[...]: — :not() заперечення
in-[...]: — контекстна залежність
max-*: — обмеження breakpoint зверху
Завдання для самоперевірки
Завдання 1.1. Опишіть, що робить кожен клас:
<li class="first:pt-0 last:pb-0 py-3">
<input class="border-gray-300 focus:border-indigo-500 focus:ring-2 invalid:border-red-500" />
<button class="bg-indigo-500 hover:bg-indigo-600 active:scale-95 disabled:opacity-50 transition-all">
<span class="opacity-0 group-hover:opacity-100 transition-opacity"></span>
</button>
</li>
Завдання 1.2. Реалізуйте форму де:
- Label стає синім при фокусі вкладеного input (
has-[:focus]:text-indigo-600) - Input має червону рамку при невалідному значенні (
invalid:border-red-400) - Кнопка Submit — сіра при
disabled, за умовою перевірки чекбоксу
Завдання 1.3. Зробіть Toggle Switch тільки через CSS (без JavaScript):
- Приховати
<input type="checkbox">черезsr-only - Label відображає перемикач: сірий при
off, індиго приon - Ковзний круглий бегунок через
peer-checked:translate-x-5
Завдання 2.1. Accordion без JavaScript.
Використовуючи <details> та <summary>:
<details class="group">
<summary class="flex items-center justify-between cursor-pointer">
Питання
<!-- Іконка повернена при open -->
<span class="group-open:rotate-180 transition-transform">▼</span>
</summary>
<div class="animate в open-стані через group-open:...">Відповідь</div>
</details>
Зробіть 3+ питання-відповіді з плавною анімацією через CSS.
Завдання 2.2. Повноцінний темний режим.
Реалізуйте сторінку з:
- Автоматичний dark через OS (
prefers-color-scheme) - Ручний через кнопку (toggle
.darkклас на<html>) - Семантичні CSS-токени:
--bg,--text,--border,--accent localStorage— зберегти вибір користувача між сесіями
Завдання 2.3. Dropdown Menu через peer + checkbox.
- Прихований checkbox
- Label = кнопка "Меню"
- Dropdown = наступний
div, видимий приpeer-checked:block - Без жодного JavaScript
Завдання 3.1. Animated Notification System.
Панель сповіщень з:
- 3 типи: success, error, info
starting:варіант для анімації появиgroup+group-hoverдля кнопки закриттяhas-[input:checked]— автоматично показує/приховує за чекбоксом- Стек сповіщень (декілька одночасно)
Завдання 3.2. Responsive Navigation.
Складний nav компонент:
- Desktop: горизонтальний, з dropdown на hover (
group-hover:block) - Mobile until
lg:— прихований, відкривається черезpeer-checkedна hamburger-checkbox - Dark mode підтримка
has-[.active]— nav підсвічується якщо є активний пункт
Попередня стаття: Кастомізація теми через @themeНаступна стаття: Типографіка та система кольорів
Кастомізація теми через @theme у Tailwind v4
CSS-first конфігурація Tailwind v4: @theme директива замість tailwind.config.js. Кастомні кольори через OKLCH, шрифти, breakpoints, spacing, тіні та анімації. Побудова власної дизайн-системи.
Типографіка та система кольорів у Tailwind v4
Повна типографічна система Tailwind v4: шрифти, масштаб, line-height, prose плагін. Кольорова палітра OKLCH, opacity modifiers, кастомні кольори та дизайн-токени. Text-wrap balance/pretty та контент-утиліти.