Варіанти: hover, focus, responsive, dark mode та нові v4
Варіанти: hover, focus, responsive, dark mode та нові v4
Що таке варіант: від CSS до декларативного UI
Щоб зрозуміти концепцію варіантів у Tailwind, необхідно спочатку звернутись до фундаментальної проблеми, яку вони вирішують. У традиційному CSS для опису різних станів елемента розробник писав окремі блоки правил: селектор для базового стану, .btn:hover {} для наведення, .btn:focus-visible {} для клавіатурного фокусу, @media (prefers-color-scheme: dark) {} для темної теми. Логіка одного елемента розпорошувалась між кількома файлами і декількома місцями в CSS.
Tailwind пропонує принципово інший підхід: уся поведінка елемента описується безпосередньо в class-атрибуті HTML. Кожен клас є комбінацією двох частин — умови та CSS-правила:
{варіант}:{утиліта} = {умова, за якої CSS застосовується} + {саме CSS-правило}
Наприклад, hover:bg-indigo-600 — це не просто колір, це декларативне твердження: «фон стає indigo-600 у момент, коли користувач наводить курсор на цей елемент». Читаючи HTML, ви бачите повну поведінку елемента без необхідності перегортати CSS-файл.
Ще важливіше те, що варіанти можна стекати — комбінувати кілька умов в один клас: dark:hover:bg-indigo-700 застосовує колір лише тоді, коли одночасно активна темна тема і елемент у стані hover. Це дозволяє описати найскладнішу умовну логіку у декларативній формі без жодного рядка JavaScript.
Варіанти взаємодії: станові псевдокласи
hover, focus, active: тріада базової взаємодії
Три найбільш вживані варіанти відповідають трьом базовим станам взаємодії користувача з елементом. hover: застосовується, коли курсор миші знаходиться над елементом. focus: — коли елемент отримав фокус (через Tab або клік). active: — у момент активного натискання, між mousedown і mouseup.
Разом вони описують повний цикл взаємодії з інтерактивним елементом:
<!-- Базова кнопка з трьома станами взаємодії -->
<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 duration-150
">
Натисніть мене
</button>
Зверніть увагу на ключову відмінність між focus: та focus-visible:. Псевдоклас :focus спрацьовує при будь-якому отриманні фокусу — включно з кліком мишею, що є небажаним для кнопок (обведення з'являтиметься після кожного кліку). Псевдоклас :focus-visible є розумнішим: він спрацьовує лише тоді, коли браузер визначає, що індикатор фокусу є корисним для користувача — тобто при навігації клавіатурою. Це робить focus-visible: стандартом accessibility у сучасному UI.
visited, disabled, checked, required, invalid
Окрім тріади основних станів, Tailwind надає варіанти для всіх стандартних CSS-псевдокласів:
<!-- Відвідані посилання отримують інший колір -->
<a href="/page" class="text-indigo-600 visited:text-purple-600 underline">
Посилання
</a>
<!-- Вимкнені елементи — зниження прозорості та зміна курсору -->
<button
class="bg-indigo-500 text-white px-4 py-2 rounded-lg
disabled:opacity-50 disabled:cursor-not-allowed
disabled:pointer-events-none"
disabled
>
Вимкнено
</button>
<!-- Checkbox — різний вигляд залежно від стану -->
<input
type="checkbox"
class="w-4 h-4 rounded border-gray-300
checked:border-indigo-500 checked:bg-indigo-500
focus-visible:ring-2 focus-visible:ring-indigo-500"
/>
<!-- Валідація email у реальному часі -->
<input
type="email"
class="border border-gray-300 rounded-lg px-3 py-2
invalid:border-red-500 invalid:bg-red-50
valid:border-green-500
focus:ring-2 focus:ring-indigo-500 focus:outline-none
transition-colors"
placeholder="name@example.com"
/>
invalid: спрацьовує лише після того, як елемент вперше втратив фокус або форма намагалась відправитись. Для активної валідації в процесі друку — поєднуйте з :not(:placeholder-shown).placeholder, file, selection: спеціальні псевдоелементи
<!-- Стилізація тексту-підказки в input -->
<input
class="placeholder:text-gray-400 placeholder:italic placeholder:text-sm"
placeholder="Введіть ваш email..."
/>
<!-- Кнопка вибору файлу — повна кастомізація без JS -->
<input
type="file"
class="file:mr-4 file:py-2 file:px-4
file:rounded-lg file:border-0
file:text-sm file:font-semibold
file:bg-indigo-50 file:text-indigo-700
hover:file:bg-indigo-100 file:cursor-pointer
file:transition-colors"
/>
<!-- Колір виділення тексту -->
<p class="selection:bg-indigo-500 selection:text-white leading-relaxed">
Виділіть цей текст мишею — ви побачите брендовий колір виділення.
Це один клас замість глобального ::selection правила.
</p>
<!DOCTYPE html>
<html lang="uk">
<head>
<meta charset="UTF-8">
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="p-8 bg-slate-50 font-sans space-y-6">
<p class="text-xs font-bold text-slate-400 uppercase tracking-widest mb-2">Стани взаємодії</p>
<!-- Кнопки з різними станами -->
<div class="bg-white rounded-2xl border border-slate-200 p-6 space-y-4">
<p class="text-xs font-semibold text-slate-400 uppercase tracking-widest">hover / active / focus-visible</p>
<div class="flex gap-3 flex-wrap">
<button class="px-5 py-2.5 bg-indigo-600 text-white font-semibold rounded-xl hover:bg-indigo-700 active:bg-indigo-800 active:scale-95 focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 transition-all text-sm">
Primary
</button>
<button class="px-5 py-2.5 border-2 border-indigo-500 text-indigo-600 font-semibold rounded-xl hover:bg-indigo-50 active:bg-indigo-100 focus-visible:ring-2 focus-visible:ring-indigo-400 focus-visible:ring-offset-2 transition-all text-sm">
Outline
</button>
<button class="px-5 py-2.5 bg-slate-100 text-slate-400 font-semibold rounded-xl disabled:opacity-50 disabled:cursor-not-allowed text-sm" disabled>
Disabled
</button>
</div>
<p class="text-xs font-semibold text-slate-400 uppercase tracking-widest mt-4">invalid / valid / focus</p>
<div class="space-y-2">
<input type="email" placeholder="name@example.com" class="w-full px-4 py-2.5 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 invalid:border-red-400 invalid:focus:ring-red-400 valid:border-green-400 transition-all" />
<p class="text-xs text-slate-400">← Введіть некоректний email, щоб побачити invalid: стан</p>
</div>
<p class="text-xs font-semibold text-slate-400 uppercase tracking-widest mt-4">selection:</p>
<p class="text-sm text-slate-700 leading-relaxed selection:bg-indigo-500 selection:text-white">
Виділіть цей текст мишею — ви побачите брендовий індиго колір виділення замість стандартного синього браузера.
</p>
</div>
</body>
</html>
🧪 Практика: інтерактивна форма
Створіть форму реєстрації з трьома полями: name (required), email (type="email"), password (minlength="8"). Застосуйте варіанти: invalid:border-red-400 для невалідного стану, valid:border-green-400 для валідного, placeholder:text-gray-400 placeholder:italic для підказок. Кнопка Submit — disabled:opacity-50 disabled:cursor-not-allowed. Протестуйте кожен стан у браузері.
Структурні псевдокласи: позиційна стилізація
first, last, odd, even: патерни списків
Структурні псевдокласи дозволяють стилізувати елементи на основі їхньої позиції у батьківському контейнері без прив'язки до конкретних CSS-класів або inline-стилів. Ці варіанти особливо корисні при роботі зі списками, таблицями та повторюваними компонентами, де кількість елементів визначається динамічно.
<ul class="divide-y divide-slate-200 border border-slate-200 rounded-xl overflow-hidden">
<!-- first: — перший елемент без верхнього відступу -->
<!-- last: — останній без нижнього відступу -->
<!-- odd: — непарні елементи з іншим фоном -->
<!-- even: — парні елементи з іншим фоном -->
<li class="py-3 px-4
first:rounded-t-xl last:rounded-b-xl
odd:bg-white even:bg-slate-50
hover:bg-indigo-50 transition-colors">
Пункт списку
</li>
<!-- ... повторюється -->
</ul>
Ці варіанти мають одну важливу характеристику: вони є CSS-native — тобто не вимагають жодного JavaScript і не залежать від кількості елементів. Якщо ви додасте або видалите елемент зі списку, стилізація автоматично оновиться.
nth-child у Tailwind v4: гнучка позиційна стилізація
Tailwind v4 суттєво розширив можливості позиційної стилізації, додавши повноцінну підтримку :nth-child() з різними форматами аргументів:
<!-- Кожен 2-й елемент (тобто парні) -->
<div class="nth-2:bg-slate-100 p-3">...</div>
<!-- Кожен 3-й елемент -->
<div class="nth-3:opacity-50 p-3">...</div>
<!-- Складний математичний вираз: кожен непарний -->
<div class="nth-[2n+1]:bg-indigo-50 p-3">...</div>
<!-- З фільтром класу: кожен 2-й елемент .card, ігноруючи решту -->
<div class="nth-[2n_of_.card]:bg-slate-50 p-3">...</div>
<!DOCTYPE html>
<html lang="uk">
<head>
<meta charset="UTF-8">
<script src="https://cdn.tailwindcss.com"></script>
<style>
.item-box {
transition: all 0.3s;
border: 1px solid #e2e8f0;
background-color: #ffffff;
}
/* Симуляція стилів в залежності від обраного варіанту */
.filter-nth-2 .item-box:nth-child(2n) {
background-color: #f5f3ff; /* brand-50 */
border-color: #a78bfa; /* brand-400 */
transform: translateY(-1px);
}
.filter-nth-3 .item-box:nth-child(3n) {
background-color: #f5f3ff;
border-color: #a78bfa;
transform: translateY(-1px);
}
.filter-nth-odd .item-box:nth-child(2n+1) {
background-color: #f5f3ff;
border-color: #a78bfa;
transform: translateY(-1px);
}
.filter-nth-of-card .item-box:nth-child(2n of .card) {
background-color: #f5f3ff;
border-color: #a78bfa;
transform: translateY(-1px);
}
</style>
</head>
<body class="p-6 bg-slate-50 font-sans flex flex-col items-center">
<div class="w-full max-w-md bg-white p-6 rounded-2xl shadow-md border border-slate-100">
<p class="text-xs font-bold text-slate-400 uppercase tracking-widest mb-4">Демонстрація nth-child варіантів у v4</p>
<!-- Filter Tabs -->
<div class="flex gap-1 mb-6 flex-wrap">
<button onclick="setFilter('nth-2')" id="btn-nth-2" class="px-2.5 py-1 text-xs font-semibold rounded bg-indigo-600 text-white shadow">nth-2</button>
<button onclick="setFilter('nth-3')" id="btn-nth-3" class="px-2.5 py-1 text-xs font-semibold rounded bg-slate-100 hover:bg-slate-200 text-slate-600">nth-3</button>
<button onclick="setFilter('nth-odd')" id="btn-nth-odd" class="px-2.5 py-1 text-xs font-semibold rounded bg-slate-100 hover:bg-slate-200 text-slate-600">nth-[2n+1]</button>
<button onclick="setFilter('nth-of-card')" id="btn-nth-of-card" class="px-2.5 py-1 text-xs font-semibold rounded bg-slate-100 hover:bg-slate-200 text-slate-600 text-center">nth-[2n of .card]</button>
</div>
<!-- List Grid -->
<div id="list-container" class="grid grid-cols-3 gap-3 filter-nth-2">
<div class="item-box p-3 rounded-xl text-center shadow-sm card">
<span class="text-xs font-bold text-slate-700 block">Item 1</span>
<span class="text-[9px] text-emerald-500 font-bold block mt-0.5">.card</span>
</div>
<div class="item-box p-3 rounded-xl text-center shadow-sm">
<span class="text-xs font-bold text-slate-700 block">Item 2</span>
<span class="text-[9px] text-slate-400 block mt-0.5">no card</span>
</div>
<div class="item-box p-3 rounded-xl text-center shadow-sm card">
<span class="text-xs font-bold text-slate-700 block">Item 3</span>
<span class="text-[9px] text-emerald-500 font-bold block mt-0.5">.card</span>
</div>
<div class="item-box p-3 rounded-xl text-center shadow-sm card">
<span class="text-xs font-bold text-slate-700 block">Item 4</span>
<span class="text-[9px] text-emerald-500 font-bold block mt-0.5">.card</span>
</div>
<div class="item-box p-3 rounded-xl text-center shadow-sm">
<span class="text-xs font-bold text-slate-700 block">Item 5</span>
<span class="text-[9px] text-slate-400 block mt-0.5">no card</span>
</div>
<div class="item-box p-3 rounded-xl text-center shadow-sm card">
<span class="text-xs font-bold text-slate-700 block">Item 6</span>
<span class="text-[9px] text-emerald-500 font-bold block mt-0.5">.card</span>
</div>
</div>
</div>
<script>
function setFilter(filter) {
const container = document.getElementById('list-container');
container.className = 'grid grid-cols-3 gap-3 filter-' + filter;
// Update button styles
const buttons = ['nth-2', 'nth-3', 'nth-odd', 'nth-of-card'];
buttons.forEach(b => {
const btn = document.getElementById('btn-' + b);
if (b === filter) {
btn.className = "px-2.5 py-1 text-xs font-semibold rounded bg-indigo-600 text-white shadow";
} else {
btn.className = "px-2.5 py-1 text-xs font-semibold rounded bg-slate-100 hover:bg-slate-200 text-slate-600";
}
});
}
</script>
</body>
</html>
Синтаксис nth-[expr] надає доступ до повної потужності CSS :nth-child(An+B) — він дозволяє описувати будь-які повторювані патерни з математичною точністю.
only, empty, first-of-type: рідше, але важливо
<!-- Показати спеціальний UI якщо елемент єдиний у батьку -->
<p class="hidden only:block text-center text-slate-400 italic py-8">
Список порожній. Додайте перший елемент.
</p>
<!-- Приховати порожній контейнер -->
<div class="empty:hidden border border-slate-200 rounded-lg p-4">
<!-- якщо тут нічого немає — div не займатиме місце -->
</div>
<!-- Перший рядок таблиці певного типу -->
<tr class="first-of-type:bg-slate-50 first-of-type:font-semibold">
<td>...</td>
</tr>
empty: визначає порожнечу суворо: пробіли та переноси рядків у HTML вважаються контентом. Якщо <div></div> — порожній, <div> </div> — ні.<!DOCTYPE html>
<html lang="uk">
<head>
<meta charset="UTF-8">
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="p-8 bg-slate-50 font-sans">
<p class="text-xs font-bold text-slate-400 uppercase tracking-widest mb-4">Структурні варіанти</p>
<div class="bg-white rounded-2xl border border-slate-200 overflow-hidden shadow-sm">
<div class="px-4 py-3 bg-slate-50 border-b border-slate-200 text-xs font-semibold text-slate-500">
odd: / even: / first: / last:
</div>
<ul class="divide-y divide-slate-100">
<li class="px-4 py-3 text-sm text-slate-700 odd:bg-white even:bg-slate-50 first:font-semibold last:text-slate-400 hover:bg-indigo-50 transition-colors flex items-center justify-between">
<span>Елемент 1 — first: font-semibold</span>
<span class="text-xs text-slate-400 font-mono">odd: bg-white</span>
</li>
<li class="px-4 py-3 text-sm text-slate-700 odd:bg-white even:bg-slate-50 hover:bg-indigo-50 transition-colors flex items-center justify-between">
<span>Елемент 2</span>
<span class="text-xs text-indigo-400 font-mono">even: bg-slate-50</span>
</li>
<li class="px-4 py-3 text-sm text-slate-700 odd:bg-white even:bg-slate-50 hover:bg-indigo-50 transition-colors flex items-center justify-between">
<span>Елемент 3</span>
<span class="text-xs text-slate-400 font-mono">odd: bg-white</span>
</li>
<li class="px-4 py-3 text-sm text-slate-700 odd:bg-white even:bg-slate-50 hover:bg-indigo-50 transition-colors flex items-center justify-between">
<span>Елемент 4</span>
<span class="text-xs text-indigo-400 font-mono">even: bg-slate-50</span>
</li>
<li class="px-4 py-3 text-sm text-slate-700 odd:bg-white even:bg-slate-50 last:text-slate-400 hover:bg-indigo-50 transition-colors flex items-center justify-between">
<span>Елемент 5 — last: text-slate-400</span>
<span class="text-xs text-slate-400 font-mono">odd: bg-white</span>
</li>
</ul>
</div>
<div class="mt-4 bg-white rounded-2xl border border-slate-200 p-4">
<p class="text-xs font-semibold text-slate-400 uppercase tracking-widest mb-3">empty: — порожній контейнер</p>
<div class="empty:hidden border-2 border-dashed border-red-300 rounded-lg p-4 text-red-500 text-sm">
<!-- Цей div порожній → empty:hidden приховає його -->
</div>
<div class="border-2 border-dashed border-green-300 rounded-lg p-4 text-green-600 text-sm">
Цей div має контент → empty:hidden не діє
</div>
</div>
</body>
</html>
🧪 Практика: таблиця з зебра-стилями
Створіть HTML-таблицю з 6+ рядками даних. Застосуйте: odd:bg-white even:bg-slate-50 до рядків, first:rounded-t-xl last:rounded-b-xl до крайніх рядків, hover:bg-indigo-50 transition-colors до кожного рядка. Спробуйте також nth-3:font-bold і переконайтеся, що кожен третій рядок виділяється жирним шрифтом.
Псевдоелементи: before, after та спеціальні
before та after: декоративний контент через CSS
Псевдоелементи ::before та ::after дозволяють вставляти декоративний контент безпосередньо через CSS, без додаткових HTML-елементів. Tailwind надає варіанти before: та after: для повного контролю над цими псевдоелементами, включно з властивістю content.
<!-- Декоративна стрілка перед текстом -->
<li class="before:content-['→'] before:mr-2 before:text-indigo-500 before:font-bold">
Пункт списку зі стрілкою
</li>
<!-- Обов'язкове поле — зірочка після label -->
<label class="after:content-['*'] after:text-red-500 after:ml-0.5 font-medium">
Email
</label>
<!-- Лічильник з data-атрибута -->
<button
class="relative before:content-[attr(data-count)]
before:absolute before:-top-1.5 before:-right-1.5
before:bg-red-500 before:text-white
before:text-xs before:font-bold
before:w-4 before:h-4 before:rounded-full
before:flex before:items-center before:justify-center"
data-count="3"
>
Сповіщення
</button>
content у before:content-['текст'] підтримує CSS-функцію attr(): before:content-[attr(data-label)] підставить значення атрибута data-label елемента.marker, first-line, first-letter, backdrop
<!-- Кастомний маркер списку -->
<ul class="list-disc marker:text-indigo-500 marker:text-lg space-y-1">
<li>Перший пункт</li>
<li>Другий пункт</li>
</ul>
<!-- Перший рядок параграфу — ефект газетної верстки -->
<p class="first-line:uppercase first-line:tracking-widest
first-line:text-indigo-600 first-line:font-semibold
leading-relaxed text-slate-700">
Перший рядок буде стилізований особливим чином.
Решта тексту виглядає звичайно.
</p>
<!-- Перша літера — класичний ефект капітелі (drop cap) -->
<p class="first-letter:text-5xl first-letter:font-black
first-letter:float-left first-letter:mr-2
first-letter:text-indigo-600 first-letter:leading-none
leading-relaxed">
Давним-давно у далекій галактиці розробники
писали CSS без utility-фреймворків...
</p>
<!-- Backdrop для нативного <dialog> елемента -->
<dialog class="backdrop:bg-slate-900/60 backdrop:backdrop-blur-sm
rounded-2xl shadow-2xl p-8">
<p>Контент діалогу з затемненим фоном</p>
</dialog>
<!DOCTYPE html>
<html lang="uk">
<head>
<meta charset="UTF-8">
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="p-6 bg-slate-50 font-sans space-y-6 flex flex-col items-center">
<div class="w-full max-w-md bg-white p-6 rounded-2xl shadow-md border border-slate-100 space-y-6">
<p class="text-xs font-bold text-slate-400 uppercase tracking-widest">Демонстрація псевдоелементів</p>
<!-- form label after: required star -->
<div>
<label class="block text-sm font-semibold text-slate-700 after:content-['*'] after:text-red-500 after:ml-0.5">
Електронна пошта (required)
</label>
<input type="email" placeholder="name@example.com" class="mt-1.5 w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:outline-none focus:border-indigo-500">
</div>
<!-- before: dynamic count badge -->
<div class="flex items-center justify-between border-t border-b border-slate-100 py-4">
<span class="text-sm font-semibold text-slate-700">Динамічний badge (before:):</span>
<button onclick="addNotification()" id="notif-btn" data-count="3" class="relative px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white font-semibold rounded-lg text-xs shadow transition-colors
before:content-[attr(data-count)] before:absolute before:-top-1.5 before:-right-1.5 before:bg-red-500 before:text-white before:text-[9px] before:font-bold before:w-4 before:h-4 before:rounded-full before:flex before:items-center before:justify-center">
Сповіщення
</button>
</div>
<!-- custom list marker -->
<div>
<p class="text-xs font-bold text-slate-400 uppercase tracking-widest mb-2">Кастомний marker:</p>
<ul class="list-disc pl-5 marker:text-indigo-500 marker:text-lg space-y-1 text-sm text-slate-600">
<li>Перший пункт</li>
<li>Другий пункт</li>
</ul>
</div>
<!-- drop cap: first-letter and first-line -->
<div>
<p class="text-xs font-bold text-slate-400 uppercase tracking-widest mb-2">first-letter & first-line:</p>
<p class="first-letter:text-4xl first-letter:font-black first-letter:float-left first-letter:mr-2 first-letter:text-indigo-600 first-letter:leading-none first-line:uppercase first-line:tracking-wider first-line:text-indigo-500 first-line:font-bold text-xs text-slate-600 leading-relaxed">
давним-давно у далекій галактиці розробники писали CSS без utility-фреймворків. Вони страждали від гігантських файлів та дублювання стилів, доки не з'явився Tailwind.
</p>
</div>
<!-- dialog backdrop trigger -->
<div class="pt-2">
<button onclick="document.getElementById('demo-dialog').showModal()" class="w-full py-2 bg-slate-100 hover:bg-slate-200 text-slate-700 font-semibold rounded-xl text-xs transition-colors border border-slate-200">
Відкрити діалог (<dialog> + backdrop:)
</button>
</div>
</div>
<!-- Modal Dialog -->
<dialog id="demo-dialog" class="backdrop:bg-slate-900/60 backdrop:backdrop-blur-sm rounded-2xl shadow-2xl p-6 border border-slate-100 max-w-xs focus:outline-none">
<h4 class="font-bold text-slate-900 text-base mb-2">Вікно діалогу</h4>
<p class="text-xs text-slate-500 leading-relaxed mb-4">Зверніть увагу на розмиття та затемнення фону позаду цього вікна. Це забезпечується класом <code class="bg-slate-100 px-1 py-0.5 rounded text-[10px]">backdrop:backdrop-blur-sm</code>.</p>
<button onclick="document.getElementById('demo-dialog').close()" class="w-full py-1.5 bg-indigo-600 hover:bg-indigo-700 text-white font-semibold rounded-lg text-xs transition-colors">
Закрити
</button>
</dialog>
<script>
function addNotification() {
const btn = document.getElementById('notif-btn');
let count = parseInt(btn.getAttribute('data-count'));
btn.setAttribute('data-count', count + 1);
}
</script>
</body>
</html>
🧪 Практика: декоративна типографіка
Реалізуйте три типографічні ефекти: (1) список із кастомними emoji-маркерами через before:content-['✦'], (2) параграф із first-letter: ефектом drop cap у стилі журнальної статті, (3) label форми із червоною зірочкою через after:content-['*'] after:text-red-500. Всі три — без жодного HTML, лише CSS через Tailwind.
group та peer: реакція на стан сусідніх і батьківських елементів
group: каскадний hover від батька до нащадків
Одна з найпотужніших можливостей системи варіантів Tailwind — це варіант group, що дозволяє дочірнім елементам реагувати на стан батьківського контейнера. Без цього механізму єдиний спосіб реалізувати таку поведінку — JavaScript. З group це вирішується виключно через CSS.
Принцип роботи: батьківський елемент отримує клас group (без жодного CSS-ефекту, лише як маркер). Дочірні елементи використовують префікс group-hover:, group-focus:, group-active: — і реагують на відповідний стан батька:
<!-- group на батьку — маркер без власних стилів -->
<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">
<!-- Іконка — змінює фон при hover батька -->
<div class="size-10 bg-indigo-100 group-hover:bg-indigo-500
rounded-lg flex items-center justify-center
text-xl transition-colors mb-3">
🎨
</div>
<!-- Заголовок — змінює колір при hover батька -->
<h3 class="font-bold text-slate-800 group-hover:text-indigo-700
transition-colors">
Дизайн-система
</h3>
<!-- Текст — змінює прозорість і колір -->
<p class="text-sm text-slate-500 group-hover:text-indigo-500/70
transition-colors mt-1">
Токени, компоненти, утиліти
</p>
<!-- Стрілка — прихована до hover, з'являється плавно -->
<span class="inline-block mt-3 text-sm font-semibold text-indigo-500
opacity-0 group-hover:opacity-100
translate-x-0 group-hover:translate-x-1
transition-all">
Детальніше →
</span>
</div>
group/{name}: іменовані групи для вкладених структур
Проблема виникає, коли group-елементи вкладені один в одного: дочірній елемент не може розрізнити, реакція якого саме батька йому потрібна — він реагуватиме на будь-який group-батько у ланцюжку. Tailwind вирішує це через іменовані групи:
<!-- Зовнішній контейнер — group/card -->
<div class="group/card p-4 bg-white rounded-xl border hover:border-indigo-200 transition-colors">
<!-- Внутрішній контейнер — group/btn -->
<div class="group/btn flex items-center gap-2 cursor-pointer">
<button class="px-3 py-1.5 bg-slate-100 rounded-lg text-sm font-medium
group-hover/btn:bg-indigo-500
group-hover/btn:text-white transition-colors">
Дія
</button>
<!-- Реагує ТІЛЬКИ на hover .group/btn, не на .group/card -->
<span class="opacity-0 group-hover/btn:opacity-100
text-xs text-indigo-500 transition-opacity">
Натисніть
</span>
</div>
<!-- Реагує ТІЛЬКИ на hover .group/card, не на .group/btn -->
<p class="mt-3 text-xs text-slate-400
opacity-0 group-hover/card:opacity-100 transition-opacity">
Картка активна
</p>
</div>
Синтаксис group/{name} та group-hover/{name} забезпечує точну прив'язку: кожен дочірній елемент явно вказує, на якого батька він реагує.
peer: реакція наступного сусіда на стан попереднього
Варіант peer вирішує симетричну задачу: наступний сусідній елемент реагує на стан попереднього. Найкласичніший приклад — toggle switch без JavaScript:
<!-- peer на input — маркер, аналогічний group -->
<input id="toggle" type="checkbox" class="peer sr-only" />
<!-- label ПІСЛЯ input у DOM — реагує на стан peer -->
<label for="toggle" class="flex items-center gap-3 cursor-pointer select-none">
<div class="relative">
<!-- Трек перемикача -->
<div class="w-11 h-6 bg-slate-300 peer-checked:bg-indigo-500
rounded-full transition-colors duration-200">
</div>
<!-- Бігунок -->
<div class="absolute top-0.5 left-0.5 size-5 bg-white rounded-full shadow
transition-transform duration-200
peer-checked:translate-x-5">
</div>
</div>
<span class="text-sm font-medium text-slate-700
peer-checked:text-indigo-600 transition-colors">
Увімкнути сповіщення
</span>
</label>
peer-елемент має стояти ДО елемента, що реагує. CSS не може «дивитись назад» на попередні сусіди. Якщо елемент стоїть після peer-*-реагуючого елемента — він не спрацює.Аналогічно до group, peer підтримує іменування: peer/name та peer-checked/name — для ситуацій із кількома peer-елементами на одному рівні.
<!DOCTYPE html>
<html lang="uk">
<head>
<meta charset="UTF-8">
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="p-8 bg-slate-50 font-sans space-y-6">
<p class="text-xs font-bold text-slate-400 uppercase tracking-widest">group та peer</p>
<!-- group-hover: картки -->
<div>
<p class="text-xs text-slate-400 font-semibold 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-9 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-400 mt-1 transition-colors">Компоненти та UI</p>
<span class="text-xs font-semibold text-indigo-500 opacity-0 group-hover:opacity-100 translate-x-0 group-hover:translate-x-1 transition-all inline-block mt-2">Детальніше →</span>
</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-9 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-400 mt-1 transition-colors">Оптимізація</p>
<span class="text-xs font-semibold text-emerald-500 opacity-0 group-hover:opacity-100 translate-x-0 group-hover:translate-x-1 transition-all inline-block mt-2">Детальніше →</span>
</div>
</div>
</div>
<!-- peer: toggle switches -->
<div class="bg-white rounded-2xl border border-slate-200 p-5">
<p class="text-xs text-slate-400 font-semibold uppercase tracking-widest mb-4">peer-checked: — Toggle Switches без JavaScript</p>
<div class="space-y-3">
<label class="flex items-center justify-between cursor-pointer p-3 rounded-xl border border-slate-200 hover:border-indigo-200 transition-colors">
<div>
<p class="text-sm font-semibold text-slate-700">Email сповіщення</p>
<p class="text-xs text-slate-400">Отримувати листи про новини</p>
</div>
<div class="relative ml-4">
<input type="checkbox" class="sr-only peer" checked />
<div class="w-11 h-6 bg-slate-300 peer-checked:bg-indigo-500 rounded-full transition-colors"></div>
<div class="absolute top-0.5 left-0.5 size-5 bg-white rounded-full shadow transition-transform peer-checked:translate-x-5"></div>
</div>
</label>
<label class="flex items-center justify-between cursor-pointer p-3 rounded-xl border border-slate-200 hover:border-indigo-200 transition-colors">
<div>
<p class="text-sm font-semibold text-slate-700">Push сповіщення</p>
<p class="text-xs text-slate-400">Сповіщення у браузері</p>
</div>
<div class="relative ml-4">
<input type="checkbox" class="sr-only peer" />
<div class="w-11 h-6 bg-slate-300 peer-checked:bg-indigo-500 rounded-full transition-colors"></div>
<div class="absolute top-0.5 left-0.5 size-5 bg-white rounded-full shadow transition-transform peer-checked:translate-x-5"></div>
</div>
</label>
</div>
</div>
</body>
</html>
🧪 Практика: інтерактивна картка та toggle
Реалізуйте два компоненти. Перший — картка товару з group: при наведенні повинні одночасно змінюватися фон, колір заголовку та з'являтися кнопка «Додати у кошик» з opacity-0 group-hover:opacity-100. Другий — список налаштувань із трьома toggle-перемикачами через peer-checked:. Жодного JavaScript — лише CSS-варіанти.
has-*: батьківський селектор у Tailwind v4
Проблема, яку вирішує :has()
До появи CSS :has() розробники не мали нативного способу стилізувати батьківський елемент на основі стану його нащадків. Єдиним рішенням був JavaScript: відстежувати стан дочірніх елементів і вручну додавати/видаляти класи на батьку. Це породжувало ненадійний, залежний від JS код для суто CSS-задач.
Варіант has-[selector]: у Tailwind v4 відкриває доступ до :has() через знайомий синтаксис префіксів:
<!-- Картка змінює layout, якщо містить зображення -->
<div class="flex flex-col has-[img]:flex-row gap-4 p-4 bg-white rounded-xl border">
<img src="photo.jpg" class="w-32 h-24 object-cover rounded-lg" />
<div>
<h3 class="font-bold">Заголовок</h3>
<p class="text-sm text-slate-500">Опис</p>
</div>
</div>
<!-- Label підсвічується при фокусі вкладеного input — без JS! -->
<label class="block text-sm font-semibold text-slate-700
has-[:focus]:text-indigo-600 transition-colors">
Email адреса
<input type="email"
class="mt-1 block w-full border border-slate-300 rounded-lg px-3 py-2
focus:outline-none focus:border-indigo-500" />
</label>
<!-- Форма підсвічує рамку, якщо є заповнений input -->
<form class="border-2 border-slate-200 rounded-xl p-4
has-[input:not(:placeholder-shown)]:border-indigo-400 transition-colors">
<input type="text" placeholder="Почніть вводити..."
class="w-full focus:outline-none text-sm" />
</form>
<!-- Nav підсвічується якщо є активний пункт -->
<nav class="bg-white border-b
has-[.active]:border-indigo-500 has-[.active]:bg-indigo-50 transition-colors">
<a href="/" class="px-4 py-3 block active">Головна</a>
<a href="/about" class="px-4 py-3 block">Про нас</a>
</nav>
Різниця між has-*, group-* та peer-*
Три механізми вирішують схожі, але різні задачі. Розуміння кожного є ключем до правильного вибору:
group. Реагує на hover, focus, active батька.peer. Реагує на checked, focus, hover попереднього.<!DOCTYPE html>
<html lang="uk">
<head>
<meta charset="UTF-8">
<script src="https://cdn.tailwindcss.com/4.0"></script>
</head>
<body class="p-6 bg-slate-50 font-sans space-y-4 max-w-md mx-auto">
<p class="text-xs font-bold text-slate-400 uppercase tracking-widest">has-[selector]: у дії</p>
<!-- Label змінює колір при focus input -->
<div class="bg-white rounded-xl p-4 border border-slate-200">
<p class="text-xs font-semibold text-slate-400 mb-3">has-[:focus] — label реагує на focus</p>
<label class="block text-sm font-semibold text-slate-600 has-[:focus]:text-indigo-600 transition-colors">
Email адреса
<input type="email" placeholder="name@example.com"
class="mt-1.5 block w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:outline-none focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500/20 transition-colors">
</label>
</div>
<!-- Форма підсвічується при наявності тексту -->
<div class="bg-white rounded-xl p-4 border border-slate-200">
<p class="text-xs font-semibold text-slate-400 mb-3">has-[input:not(:placeholder-shown)] — форма реагує на текст</p>
<form class="border-2 border-slate-200 rounded-lg p-3 has-[input:not(:placeholder-shown)]:border-indigo-400 has-[input:not(:placeholder-shown)]:bg-indigo-50/30 transition-all">
<input type="text" placeholder="Почніть вводити..."
class="w-full focus:outline-none text-sm bg-transparent">
</form>
<p class="text-xs text-slate-400 mt-2">Введіть текст → рамка стане синьою</p>
</div>
<!-- has-[img] змінює layout -->
<div class="bg-white rounded-xl p-4 border border-slate-200">
<p class="text-xs font-semibold text-slate-400 mb-3">has-[img] — зміна layout при наявності зображення</p>
<div class="flex flex-col has-[img]:flex-row gap-3 p-3 bg-slate-50 rounded-lg">
<img src="https://picsum.photos/seed/demo/80/60" class="rounded-lg w-20 h-15 object-cover" />
<div>
<p class="text-sm font-semibold text-slate-700">Заголовок статті</p>
<p class="text-xs text-slate-400 mt-1">Layout змінився на row, бо є img всередині</p>
</div>
</div>
</div>
</body>
</html>
🧪 Практика: розумна форма із has-*
Реалізуйте форму пошуку, де: (1) <label> змінює колір при фокусі вкладеного <input> через has-[:focus]:text-indigo-600, (2) блок результатів показується тільки якщо є текст через has-[input:not(:placeholder-shown)]:block (за замовчуванням hidden), (3) кнопка очищення з'являється аналогічно. Жодного JavaScript.
not-* та in-*: нові варіанти Tailwind v4
not-*: умова-заперечення
Варіант not-* застосовує стилі лише тоді, коли задана умова не виконується. Він відображає CSS :not() псевдоклас і з'явився у Tailwind v4:
<!-- Усі елементи крім першого мають верхній розділювач -->
<li class="not-first:border-t border-slate-200 py-3 px-4">...</li>
<!-- Всі кнопки крім disabled мають hover-ефект -->
<button class="bg-indigo-500 not-disabled:hover:bg-indigo-600
disabled:opacity-50 transition-colors">
Дія
</button>
<!-- Посилання без класу .active — приглушені -->
<a class="not-[.active]:opacity-60 not-[.active]:hover:opacity-100
transition-opacity">
Пункт навігації
</a>
<!-- Всі input крім checkbox і radio -->
<input class="not-[type=checkbox]:not-[type=radio]:border not-[type=checkbox]:not-[type=radio]:rounded-lg">
not-* особливо корисний у поєднанні з first: і last: як альтернативний спосіб запису: not-first:border-t є семантично чіткішим за [&:not(:first-child)]:border-t.
in-*: контекстна залежність
Варіант in-[selector]: застосовує стилі, коли елемент знаходиться всередині певного контейнера. На відміну від group-*, він не вимагає явного маркування батька:
<!-- Посилання всередині .prose мають підкреслення -->
<a class="text-indigo-600
in-[.prose]:text-slate-700 in-[.prose]:underline in-[.prose]:underline-offset-2">
Посилання
</a>
<!-- Кнопка в nav vs у картці — різний розмір -->
<button class="font-semibold text-sm
in-[nav]:text-xs in-[nav]:px-2 in-[nav]:py-1
in-[.card]:text-base in-[.card]:px-4 in-[.card]:py-2">
Кнопка
</button>
<!-- Текст усередині selected item -->
<span class="text-slate-600 in-[[aria-selected=true]]:text-white in-[[aria-selected=true]]:font-semibold">
Пункт списку
</span>
<!DOCTYPE html>
<html lang="uk">
<head>
<meta charset="UTF-8">
<script src="https://cdn.tailwindcss.com"></script>
<style>
/* Імітуємо нові варіанти v4 not-* та in-* */
/* not-first:border-t */
.not-first-border > li {
border-top: 1px solid #e2e8f0;
}
.not-first-border > li:first-child {
border-top: none;
}
/* not-disabled:hover */
.btn-not-disabled:not(:disabled):hover {
background-color: #4f46e5; /* indigo-600 */
}
/* in-[nav] */
nav .context-btn {
font-size: 0.75rem; /* text-xs */
padding: 0.25rem 0.5rem; /* px-2 py-1 */
background-color: transparent;
color: #e2e8f0;
border: 1px solid #475569;
}
nav .context-btn:hover {
background-color: #334155;
}
/* in-[.card] */
.card .context-btn {
font-size: 1rem; /* text-base */
padding: 0.5rem 1rem; /* px-4 py-2 */
background-color: #6366f1; /* indigo-500 */
color: #ffffff;
border: none;
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
}
.card .context-btn:hover {
background-color: #4f46e5;
}
</style>
</head>
<body class="p-6 bg-slate-50 font-sans space-y-6 flex flex-col items-center">
<div class="w-full max-w-md bg-white p-6 rounded-2xl shadow-md border border-slate-100 space-y-6">
<div>
<p class="text-xs font-bold text-slate-400 uppercase tracking-widest mb-3">Демонстрація not-first та not-disabled</p>
<!-- list not-first:border-t -->
<ul class="not-first-border divide-y-0 bg-slate-50 rounded-xl overflow-hidden text-xs text-slate-600 border border-slate-200">
<li class="px-4 py-2.5">Пункт 1 (немає верхньої рамки)</li>
<li class="px-4 py-2.5">Пункт 2 (not-first: border-t)</li>
<li class="px-4 py-2.5">Пункт 3 (not-first: border-t)</li>
</ul>
<!-- buttons not-disabled:hover -->
<div class="flex gap-3 mt-4">
<button class="btn-not-disabled px-4 py-2 bg-indigo-500 text-white font-semibold rounded-lg text-xs shadow transition-colors cursor-pointer">
Активна дія (Hover me)
</button>
<button disabled class="btn-not-disabled px-4 py-2 bg-indigo-500 text-white font-semibold rounded-lg text-xs shadow opacity-50 cursor-not-allowed">
Вимкнено (No hover)
</button>
</div>
</div>
<div class="border-t border-slate-100 pt-4">
<p class="text-xs font-bold text-slate-400 uppercase tracking-widest mb-3">Демонстрація in-[selector]</p>
<!-- Button inside nav -->
<div class="space-y-4">
<div class="text-[10px] text-slate-400 font-semibold mb-1">1. Кнопка всередині <nav> (in-[nav]:)</div>
<nav class="bg-slate-800 p-3 flex items-center justify-between rounded-xl">
<span class="text-xs text-white font-bold">Logo</span>
<button class="context-btn font-semibold rounded transition-colors">Кнопка</button>
</nav>
<!-- Button inside .card -->
<div class="text-[10px] text-slate-400 font-semibold mb-1">2. Кнопка всередині .card (in-[.card]:)</div>
<div class="card bg-slate-50 p-4 rounded-xl border border-slate-200 flex items-center justify-between">
<span class="text-xs text-slate-700 font-semibold">Продукт А</span>
<button class="context-btn font-semibold rounded-lg transition-colors">Кнопка</button>
</div>
</div>
</div>
</div>
</body>
</html>
in-[selector]: генерує CSS вигляду .selector .element. Це означає, що він шукає будь-якого предка з вказаним селектором, а не лише прямого батька. Якщо потрібен лише прямий батько — використовуйте [.parent_&]:.🧪 Практика: контекстно-залежний компонент
Створіть компонент <Badge> (бейдж), який виглядає по-різному залежно від контексту: у <nav> — маленький і прозорий (in-[nav]:text-xs in-[nav]:opacity-75), у .card — стандартний розмір, у .hero — великий і виразний. Один HTML-елемент — три різних вигляди через in-*.
starting:: анімація першої появи в DOM
Проблема анімації при mount
До появи @starting-style CSS не мав нативного способу анімувати елемент у момент його першої появи в DOM. Якщо елемент вже існував і отримував клас — transition спрацьовував. Але якщо елемент щойно додавався через display: block або visibility: visible — браузер не знав, від якого стану анімувати.
CSS @starting-style вирішує це: він дозволяє задати початковий стан для CSS-переходу при першій появі елемента. Tailwind v4 надає цю можливість через варіант starting::
<!-- Елемент появляється анімовано — без JS! -->
<div class="opacity-100 translate-y-0 transition-all duration-400 ease-out
starting:opacity-0 starting:translate-y-4">
Цей блок анімується при появі в DOM
</div>
<!-- Dialog з анімацією появи -->
<dialog class="opacity-100 scale-100 transition-all duration-300
starting:opacity-0 starting:scale-95"
open>
<p>Контент діалогу — появляється з масштабуванням</p>
</dialog>
<!-- Notification toast -->
<div class="opacity-100 translate-x-0 transition-all duration-500 ease-[cubic-bezier(0.16,1,0.3,1)]
starting:opacity-0 starting:translate-x-8">
✅ Збережено успішно
</div>
Підтримка браузерів та fallback
На момент виходу Tailwind v4 @starting-style підтримувався Chrome 117+, Firefox 129+, Safari 17.5+. Для старіших браузерів варіант starting: просто ігнорується — елемент з'являється без анімації, що є коректним graceful degradation.
<!DOCTYPE html>
<html lang="uk">
<head>
<meta charset="UTF-8">
<script src="https://cdn.tailwindcss.com/4.0"></script>
<style>
@keyframes pop-in {
from { opacity: 0; transform: scale(0.9) translateY(8px); }
to { opacity: 1; transform: scale(1) translateY(0); }
}
.demo-card { animation: pop-in 0.4s cubic-bezier(0.16,1,0.3,1) both; }
.delay-1 { animation-delay: 0.08s; }
.delay-2 { animation-delay: 0.16s; }
.delay-3 { animation-delay: 0.24s; }
</style>
</head>
<body class="p-8 bg-slate-50 font-sans">
<p class="text-xs font-bold text-slate-400 uppercase tracking-widest mb-4">starting: — анімація першої появи</p>
<p class="text-xs text-slate-400 mb-6">Перезавантажте сторінку, щоб побачити анімацію знову ↺</p>
<div class="grid grid-cols-3 gap-4">
<div class="demo-card bg-white rounded-2xl border border-slate-200 shadow-sm p-5 text-center">
<div class="text-3xl mb-3">🎨</div>
<p class="font-bold text-slate-800 text-sm">Дизайн</p>
<p class="text-xs text-slate-400 mt-1">starting:opacity-0 starting:scale-90</p>
</div>
<div class="demo-card delay-1 bg-white rounded-2xl border border-slate-200 shadow-sm p-5 text-center">
<div class="text-3xl mb-3">⚡</div>
<p class="font-bold text-slate-800 text-sm">Швидкість</p>
<p class="text-xs text-slate-400 mt-1">animation-delay: 80ms</p>
</div>
<div class="demo-card delay-2 bg-white rounded-2xl border border-slate-200 shadow-sm p-5 text-center">
<div class="text-3xl mb-3">🚀</div>
<p class="font-bold text-slate-800 text-sm">Масштаб</p>
<p class="text-xs text-slate-400 mt-1">animation-delay: 160ms</p>
</div>
</div>
<div class="demo-card delay-3 mt-6 p-4 bg-green-50 border border-green-200 rounded-xl flex items-center gap-3">
<span class="text-green-500 text-xl">✅</span>
<div>
<p class="text-sm font-semibold text-green-800">Notification toast</p>
<p class="text-xs text-green-600">starting:opacity-0 starting:translate-x-8</p>
</div>
</div>
</body>
</html>
🧪 Практика: анімована поява елементів
Реалізуйте список новин (3 картки) з ефектом появи через starting:. Кожна картка має мати starting:opacity-0 starting:translate-y-4 і різний animation-delay (0ms, 100ms, 200ms). Також реалізуйте <dialog> зі starting:opacity-0 starting:scale-95 — нативна анімація відкриття без JavaScript.
Адаптивні варіанти: mobile-first система breakpoints
Філософія mobile-first та breakpoint-варіанти
Tailwind реалізує mobile-first підхід до адаптивного дизайну, що означає: стилі без breakpoint-префіксу застосовуються на всіх розмірах екрану (включно з мобільними), а breakpoint-варіанти застосовують стилі від визначеної ширини і вище:
| Варіант | CSS-умова | Значення |
|---|---|---|
| (none) | — | всі розміри |
sm: | min-width: 40rem | ≥ 640px |
md: | min-width: 48rem | ≥ 768px |
lg: | min-width: 64rem | ≥ 1024px |
xl: | min-width: 80rem | ≥ 1280px |
2xl: | min-width: 96rem | ≥ 1536px |
Читати responsive-клас потрібно як «від цього breakpoint — такий стиль»:
<!-- Сітка: мобільна 1 колонка → планшет 2 → десктоп 4 -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<!-- Текст: малий на мобільному, великий на десктопі -->
<h1 class="text-2xl sm:text-3xl lg:text-5xl font-black leading-tight">
Адаптивний заголовок
</h1>
<!-- Flex: вертикальний на мобільному, горизонтальний на md+ -->
<div class="flex flex-col md:flex-row items-start md:items-center gap-4">
...
</div>
<!-- Видимість: приховано на мобільному, видно на lg+ -->
<nav class="hidden lg:flex items-center gap-6">
...
</nav>
</div>
Ключовий принцип: починайте зі стилів для мобільного, потім додавайте варіанти для більших екранів. Не навпаки. Це відповідає порядку CSS-медіа-запитів і забезпечує коректне каскадування.
max-*: варіанти обмеження зверху (Tailwind v4)
До Tailwind v4 не існувало зручного способу застосувати стиль «лише до певного breakpoint» — доводилось поєднувати два варіанти або використовувати довільні значення. Tailwind v4 ввів max-*-варіанти, що відповідають max-width медіа-запитам:
<!-- Приховано тільки на мобільному (до sm) -->
<div class="max-sm:hidden">Видно тільки від sm і вище</div>
<!-- Різний padding: великий на мобільному, нормальний на md+ -->
<section class="max-md:px-6 px-0">...</section>
<!-- Тільки між md і lg -->
<div class="hidden md:block lg:hidden">Тільки на md</div>
<!-- Або через max-lg: -->
<div class="hidden md:max-lg:block">Тільки на md (md ≤ viewport < lg)</div>
<!-- Текст: великий тільки на мобільному (до sm) -->
<h1 class="text-4xl max-sm:text-2xl font-black">
Великий на мобільному
</h1>
Синтаксис md:max-lg:block — це стекання двох breakpoint-варіантів, що разом утворюють вікно від md до lg.
<!DOCTYPE html>
<html lang="uk">
<head>
<meta charset="UTF-8">
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="p-6 bg-slate-50 font-sans">
<p class="text-xs font-bold text-slate-400 uppercase tracking-widest mb-4">Адаптивна сітка</p>
<p class="text-xs text-slate-400 mb-4">Змінюйте ширину вікна браузера, щоб побачити breakpoints</p>
<!-- Адаптивна сітка -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3 mb-6">
<div class="bg-white p-4 rounded-xl border border-slate-200 shadow-sm">
<div class="text-xs font-mono text-indigo-500 mb-1">cols-1 → sm:cols-2 → lg:cols-3</div>
<p class="font-semibold text-slate-800 text-sm">Картка 1</p>
</div>
<div class="bg-white p-4 rounded-xl border border-slate-200 shadow-sm">
<div class="text-xs font-mono text-indigo-500 mb-1">grid-cols-1</div>
<p class="font-semibold text-slate-800 text-sm">Картка 2</p>
</div>
<div class="bg-white p-4 rounded-xl border border-slate-200 shadow-sm max-sm:hidden">
<div class="text-xs font-mono text-emerald-500 mb-1">max-sm:hidden</div>
<p class="font-semibold text-slate-800 text-sm">Картка 3 — прихована на мобільному</p>
</div>
</div>
<!-- Адаптивний текст -->
<div class="bg-white rounded-2xl border border-slate-200 p-5">
<p class="text-xs font-bold text-slate-400 uppercase tracking-widest mb-3">Адаптивна типографіка</p>
<h2 class="text-2xl sm:text-3xl lg:text-4xl font-black text-slate-900 leading-tight mb-2">
text-2xl sm:text-3xl lg:text-4xl
</h2>
<p class="text-sm sm:text-base text-slate-500">
text-sm → sm:text-base: менший на мобільному, більший від sm.
</p>
<div class="mt-4 flex flex-col sm:flex-row gap-3">
<button class="px-4 py-2 bg-indigo-500 text-white text-sm font-semibold rounded-lg w-full sm:w-auto">
flex-col → sm:flex-row
</button>
<button class="px-4 py-2 border border-slate-300 text-slate-600 text-sm font-semibold rounded-lg w-full sm:w-auto">
w-full → sm:w-auto
</button>
</div>
</div>
</body>
</html>
🧪 Практика: повністю адаптивний лендінг
Реалізуйте hero-секцію лендінгу: мобільний — центрований текст, flex-col, великий шрифт. sm: — вирівнювання ліворуч, трохи менший шрифт. lg: — двоколонковий layout (grid-cols-2), зображення поряд із текстом. Кнопки: w-full sm:w-auto. Навбар: на мобільному hidden, на lg: — flex.
Dark Mode варіант: від prefers-color-scheme до семантичних токенів
Автоматичний dark mode через системні налаштування
За замовчуванням варіант dark: у Tailwind v4 відповідає CSS медіа-запиту @media (prefers-color-scheme: dark). Це означає, що стилі з dark: застосовуються автоматично, якщо операційна система або браузер користувача налаштовані на темний режим:
<!-- Автоматичний dark: через OS -->
<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>
<a href="#" class="text-indigo-600 dark:text-indigo-400">Посилання</a>
</div>
Цей підхід найпростіший в реалізації, але має суттєвий недолік: користувач не може вручну перемкнути тему всередині вашого сайту незалежно від системних налаштувань.
Ручне перемикання через CSS selector
Для ручного перемикання теми у main.css потрібно переоголосити варіант dark: через @custom-variant:
@import 'tailwindcss';
/* dark: тепер спрацьовує за наявністю .dark на батьківському елементі */
@custom-variant dark (&:is(.dark *));
<html class="dark">
<!-- .dark на <html> — весь документ у темному режимі -->
<body>
<div class="bg-white dark:bg-slate-900">...</div>
</body>
</html>
// Перемикання теми одним рядком
document.documentElement.classList.toggle('dark')
// Збереження в localStorage
const isDark = document.documentElement.classList.toggle('dark')
localStorage.setItem('theme', isDark ? 'dark' : 'light')
// Відновлення при завантаженні
if (localStorage.getItem('theme') === 'dark' ||
(!localStorage.getItem('theme') && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark')
}
Архітектурний підхід: семантичні токени замість dark: скрізь
Додавання dark:-дублікату до кожного кольорового класу — це антипатерн, що призводить до роздутого і важкого в обслуговуванні HTML. Уявіть картку з 15 кольоровими класами: їх треба продублювати всі. А якщо потрібно змінити колір фону темної теми — оновити кожен елемент окремо.
Правильний підхід — семантичні CSS-токени, де dark: застосовується лише один раз — до :root:
/* main.css */
@import 'tailwindcss';
@custom-variant dark (&:is(.dark *));
@layer base {
:root {
/* Семантичні токени — light theme */
--color-bg: oklch(1 0 0); /* white */
--color-surface: oklch(0.976 0.002 247); /* slate-50 */
--color-text: oklch(0.129 0.042 264); /* slate-900 */
--color-muted: oklch(0.554 0.046 257); /* slate-500 */
--color-border: oklch(0.928 0.006 264); /* slate-200 */
--color-accent: oklch(0.585 0.233 277); /* indigo-500 */
}
.dark {
/* Ті самі токени — інші значення */
--color-bg: oklch(0.129 0.042 264); /* slate-900 */
--color-surface: oklch(0.208 0.042 264); /* slate-800 */
--color-text: oklch(0.967 0.001 286); /* slate-100 */
--color-muted: oklch(0.704 0.04 256); /* slate-400 */
--color-border: oklch(0.372 0.044 264); /* slate-700 */
--color-accent: oklch(0.673 0.182 277); /* indigo-400 */
}
}
<!-- Без єдиного dark: класу! Все через CSS-змінні -->
<div class="bg-(--color-bg) text-(--color-text) border border-(--color-border)">
<h1>Автоматична тема</h1>
<p class="text-(--color-muted)">Текст адаптується автоматично</p>
<button class="bg-(--color-accent) text-white px-4 py-2 rounded-lg">
Акцентна кнопка
</button>
</div>
bg-(--color-bg) — синтаксис Tailwind v4 для CSS Custom Properties. Він генерує background-color: var(--color-bg). Еквівалент старішого синтаксису bg-[var(--color-bg)].<!DOCTYPE html>
<html lang="uk" id="root">
<head>
<meta charset="UTF-8">
<script src="https://cdn.tailwindcss.com"></script>
<style>
:root {
--color-bg: #f8fafc; --color-surface: #ffffff;
--color-text: #0f172a; --color-muted: #64748b;
--color-border: #e2e8f0; --color-accent: #8b5cf6;
}
.dark {
--color-bg: #0f172a; --color-surface: #1e293b;
--color-text: #f1f5f9; --color-muted: #94a3b8;
--color-border: #334155; --color-accent: #a78bfa;
}
body { background: var(--color-bg); color: var(--color-text); transition: background .3s, color .3s; }
.surface { background: var(--color-surface); border: 1px solid var(--color-border); transition: all .3s; }
.muted-text { color: var(--color-muted); }
.accent-bg { background: var(--color-accent); }
.accent-text { color: var(--color-accent); }
.border-token { border-color: var(--color-border); }
</style>
</head>
<body class="p-6 font-sans" style="min-height:100vh">
<div class="flex items-center justify-between mb-6">
<p class="text-xs font-bold uppercase tracking-widest muted-text">Dark Mode через семантичні токени</p>
<button onclick="document.getElementById('root').classList.toggle('dark')"
class="px-3 py-1.5 text-xs font-semibold rounded-lg surface border-token border transition-all">
🌙 Переключити
</button>
</div>
<div class="surface rounded-2xl p-5 mb-4">
<div class="flex items-start gap-4">
<div class="size-12 accent-bg rounded-xl flex items-center justify-center text-white text-xl font-black flex-shrink-0">A</div>
<div>
<h2 class="font-bold text-base mb-1">Заголовок картки</h2>
<p class="text-sm muted-text leading-relaxed">Текст адаптується автоматично через CSS-змінні. Жодного dark: класу в HTML.</p>
</div>
</div>
<div class="mt-4 pt-4 border-t border-token flex gap-2">
<button class="px-4 py-2 accent-bg text-white text-sm font-semibold rounded-lg transition-opacity hover:opacity-90">Основна дія</button>
<button class="px-4 py-2 text-sm font-semibold rounded-lg surface border-token border muted-text transition-all hover:accent-text">Додатково</button>
</div>
</div>
<div class="grid grid-cols-3 gap-3">
<div class="surface rounded-xl p-3 text-center">
<p class="text-lg font-black accent-text">98%</p>
<p class="text-xs muted-text mt-0.5">Uptime</p>
</div>
<div class="surface rounded-xl p-3 text-center">
<p class="text-lg font-black accent-text">4.2ms</p>
<p class="text-xs muted-text mt-0.5">Response</p>
</div>
<div class="surface rounded-xl p-3 text-center">
<p class="text-lg font-black accent-text">12k</p>
<p class="text-xs muted-text mt-0.5">Users</p>
</div>
</div>
</body>
</html>
🧪 Практика: повноцінний темний режим
Реалізуйте сторінку профілю з: (1) @custom-variant dark (&:is(.dark *)) у CSS, (2) семантичними токенами у :root та .dark, (3) кнопкою-перемикачем, що зберігає вибір у localStorage та враховує системний prefers-color-scheme при першому завантаженні. Жодного dark: класу у HTML.
Стекання варіантів: комбінування умов
Принцип стекання
Варіанти можна комбінувати в ланцюжки будь-якої довжини. Кожен наступний варіант додає нову умову:
<!-- dark + hover: -->
<button class="bg-indigo-500 dark:bg-indigo-400
hover:bg-indigo-600 dark:hover:bg-indigo-300">
Кнопка
</button>
<!-- md + dark + hover: — три умови одночасно -->
<div class="md:dark:hover:bg-slate-700 transition-colors">
Активно тільки на md+, у dark-режимі, при наведенні
</div>
<!-- group-hover + dark: -->
<div class="group ...">
<span class="opacity-0 group-hover:opacity-100
dark:group-hover:opacity-80 transition-opacity">
Текст з'являється при hover, приглушений у dark
</span>
</div>
<!-- focus-visible + dark: — accessibility у dark-режимі -->
<input class="focus-visible:ring-2 focus-visible:ring-indigo-500
dark:focus-visible:ring-indigo-400 focus-visible:outline-none" />
<!-- lg + peer-checked: — адаптивна реакція на peer -->
<div class="hidden lg:peer-checked:block">
Показується при lg+, якщо peer перевірений
</div>
<!DOCTYPE html>
<html lang="uk">
<head>
<meta charset="UTF-8">
<script src="https://cdn.tailwindcss.com"></script>
<style>
/* Базові стилі для симулятора */
.stacked-container {
transition: all 0.3s;
background-color: #ffffff;
color: #1e293b;
border-color: #e2e8f0;
}
.stacked-btn {
background-color: #6366f1; /* indigo-500 */
color: #ffffff;
transition: all 0.3s;
}
/* 1. hover:bg-indigo-600 */
.stacked-btn:hover {
background-color: #4f46e5; /* indigo-600 */
}
/* 2. dark:hover:bg-indigo-300 */
.dark-mode .stacked-container {
background-color: #0f172a; /* slate-900 */
color: #ffffff;
border-color: #1e293b;
}
.dark-mode .stacked-btn {
background-color: #818cf8; /* indigo-400 */
}
.dark-mode .stacked-btn:hover {
background-color: #cbd5e1; /* slate-300 */
color: #0f172a;
}
/* 3. md:dark:hover:bg-amber-400 */
.desktop-size.dark-mode .stacked-btn:hover {
background-color: #fbbf24; /* amber-400 */
color: #0f172a;
}
</style>
</head>
<body class="p-6 bg-slate-50 font-sans flex flex-col items-center">
<div id="sim-parent" class="w-full max-w-md stacked-container border p-6 rounded-2xl shadow-md space-y-6">
<div class="flex justify-between items-center">
<p class="text-xs font-bold text-slate-400 uppercase tracking-widest">Стекання варіантів</p>
<div class="flex gap-1.5 text-[9px] font-bold font-mono">
<span id="label-size" class="px-2 py-0.5 rounded bg-slate-100 text-slate-600 border border-slate-200">Mobile</span>
<span id="label-theme" class="px-2 py-0.5 rounded bg-slate-100 text-slate-600 border border-slate-200">Light</span>
</div>
</div>
<!-- Target Button -->
<div class="text-center p-6 bg-slate-50/50 dark:bg-slate-800/30 rounded-xl border border-dashed border-slate-200 dark:border-slate-700">
<button class="stacked-btn px-6 py-2.5 font-semibold rounded-xl text-sm shadow cursor-pointer">
Наведіть на мене
</button>
<p class="text-[10px] text-slate-400 mt-3 leading-relaxed">
Спробуйте наведення при різних режимах: <br>
Light: hover → Indigo-600 | Dark: hover → Slate-300 | Desktop+Dark: hover → Amber-400
</p>
</div>
<!-- Controls -->
<div class="grid grid-cols-2 gap-2 text-xs">
<button onclick="toggleTheme()" class="px-3 py-2 bg-white hover:bg-slate-100 border border-slate-200 font-semibold rounded-lg shadow-sm text-slate-700 transition-colors">
Toggle Dark Mode
</button>
<button onclick="toggleSize()" class="px-3 py-2 bg-white hover:bg-slate-100 border border-slate-200 font-semibold rounded-lg shadow-sm text-slate-700 transition-colors">
Toggle Size (md+)
</button>
</div>
</div>
<script>
let isDark = false;
let isDesktop = false;
function toggleTheme() {
const parent = document.getElementById('sim-parent');
const label = document.getElementById('label-theme');
isDark = !isDark;
if (isDark) {
parent.classList.add('dark-mode');
label.innerText = 'Dark';
label.className = 'px-2 py-0.5 rounded bg-slate-800 text-slate-200 border border-slate-700';
} else {
parent.classList.remove('dark-mode');
label.innerText = 'Light';
label.className = 'px-2 py-0.5 rounded bg-slate-100 text-slate-600 border border-slate-200';
}
}
function toggleSize() {
const parent = document.getElementById('sim-parent');
const label = document.getElementById('label-size');
isDesktop = !isDesktop;
if (isDesktop) {
parent.classList.add('desktop-size');
label.innerText = 'Desktop (md+)';
label.className = 'px-2 py-0.5 rounded bg-amber-100 text-amber-800 border border-amber-200';
} else {
parent.classList.remove('desktop-size');
label.innerText = 'Mobile';
label.className = 'px-2 py-0.5 rounded bg-slate-100 text-slate-600 border border-slate-200';
}
}
</script>
</body>
</html>
Порядок варіантів у класі не має значення для CSS-пріоритету, але за конвенцією рекомендується: responsive:state:pseudo:. Tailwind гарантує коректну генерацію CSS незалежно від порядку.
@layer components з @apply для складних комбінацій: @apply dark:hover:bg-slate-800 md:dark:hover:bg-slate-900.Довільні варіанти: вихід за межі вбудованих
Коли вбудованих варіантів не вистачає
Tailwind охоплює 95% типових потреб через вбудовані варіанти. Для решти — [&:selector]: синтаксис довільних варіантів, де & означає «поточний елемент»:
<!-- nth-child без утиліти — конкретно 3-й елемент -->
<li class="[&:nth-child(3)]:font-bold [&:nth-child(3)]:text-indigo-600">
Я жирний і синій, тільки якщо є 3-м
</li>
<!-- Стилізувати вкладені p через батьківський елемент -->
<div class="[&_p]:text-slate-600 [&_p]:leading-relaxed [&_p]:mb-4">
<p>Цей параграф отримає стилі через довільний варіант</p>
<p>І цей також — без class на кожному p</p>
</div>
<!-- Media query для hover-спроможних пристроїв (не touch) -->
<div class="[@media(hover:hover)]:hover:opacity-75">
Hover-ефект тільки там, де він реально є (не на сенсорних)
</div>
<!-- @supports — застосувати стилі якщо CSS-можливість підтримується -->
<div class="[@supports(display:grid)]:grid [@supports(display:grid)]:grid-cols-3">
Grid-макет, тільки якщо підтримується grid
</div>
<!-- data-атрибут — стан через атрибут -->
<div class="data-[state=open]:rotate-180 transition-transform"
data-state="open">
Іконка-стрілка обертається при data-state="open"
</div>
<!-- aria-атрибут -->
<button class="aria-expanded:bg-indigo-50 aria-expanded:text-indigo-700"
aria-expanded="true">
Кнопка accordion — активна при aria-expanded="true"
</button>
data-* та aria-* варіанти
Два найпопулярніших типи довільних варіантів — data-[attr=value]: та aria-[attr=value]:. Вони дозволяють реалізувати state-based стилізацію через HTML-атрибути замість JavaScript-класів:
<!-- UI Library компонент — стан через data-атрибут -->
<li class="px-3 py-2 rounded-lg
data-[active=true]:bg-indigo-50
data-[active=true]:text-indigo-700
data-[active=true]:font-semibold
text-slate-600 cursor-pointer transition-colors"
data-active="true">
Активний пункт
</li>
<!-- Headless UI / Radix UI pattern -->
<button class="aria-[pressed=true]:bg-indigo-500 aria-[pressed=true]:text-white
px-4 py-2 rounded-lg border border-slate-300 transition-colors"
aria-pressed="false">
Toggle Button
</button>
<!DOCTYPE html>
<html lang="uk">
<head>
<meta charset="UTF-8">
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="p-8 bg-slate-50 font-sans space-y-5">
<p class="text-xs font-bold text-slate-400 uppercase tracking-widest">Довільні варіанти та data-*</p>
<!-- [&_p]: -->
<div class="bg-white rounded-xl border border-slate-200 p-5">
<p class="text-xs font-semibold text-slate-400 mb-3">[&_p]: — стилізація вкладених p</p>
<div class="[&_p]:text-slate-600 [&_p]:leading-relaxed [&_p]:text-sm [&_p]:mb-2 [&_p:last-child]:mb-0">
<p>Перший параграф — отримав стилі через довільний варіант батька.</p>
<p>Другий параграф — аналогічно, без class на кожному p.</p>
<p>Третій параграф — mb-0 через last-child.</p>
</div>
</div>
<!-- data-[state]: -->
<div class="bg-white rounded-xl border border-slate-200 p-5">
<p class="text-xs font-semibold text-slate-400 mb-3">data-[state=active]: — стан через data-атрибут</p>
<ul class="space-y-1">
<li class="px-3 py-2 rounded-lg text-sm cursor-pointer transition-colors
data-[state=active]:bg-indigo-50 data-[state=active]:text-indigo-700 data-[state=active]:font-semibold
text-slate-600 hover:bg-slate-50"
data-state="active">
Активний пункт (data-state="active")
</li>
<li class="px-3 py-2 rounded-lg text-sm cursor-pointer transition-colors
data-[state=active]:bg-indigo-50 data-[state=active]:text-indigo-700 data-[state=active]:font-semibold
text-slate-600 hover:bg-slate-50"
data-state="inactive">
Неактивний пункт (data-state="inactive")
</li>
<li class="px-3 py-2 rounded-lg text-sm cursor-pointer transition-colors
data-[state=active]:bg-indigo-50 data-[state=active]:text-indigo-700 data-[state=active]:font-semibold
text-slate-600 hover:bg-slate-50"
data-state="inactive">
Ще один неактивний
</li>
</ul>
</div>
<!-- aria-pressed -->
<div class="bg-white rounded-xl border border-slate-200 p-5">
<p class="text-xs font-semibold text-slate-400 mb-3">aria-[pressed=true]: — стан через ARIA</p>
<div class="flex gap-2">
<button onclick="this.setAttribute('aria-pressed', this.getAttribute('aria-pressed')==='true' ? 'false' : 'true')"
class="px-4 py-2 rounded-lg border border-slate-300 text-sm font-medium transition-all
aria-[pressed=true]:bg-indigo-500 aria-[pressed=true]:text-white aria-[pressed=true]:border-indigo-500
text-slate-700 hover:border-slate-400"
aria-pressed="false">
Bold
</button>
<button onclick="this.setAttribute('aria-pressed', this.getAttribute('aria-pressed')==='true' ? 'false' : 'true')"
class="px-4 py-2 rounded-lg border border-slate-300 text-sm italic transition-all
aria-[pressed=true]:bg-indigo-500 aria-[pressed=true]:text-white aria-[pressed=true]:border-indigo-500
text-slate-700 hover:border-slate-400"
aria-pressed="false">
Italic
</button>
</div>
</div>
</body>
</html>
🧪 Практика: компонент із довільними варіантами
Реалізуйте список вкладок (tabs) де: активна вкладка визначається через data-[state=active]: атрибут (не class), клік через мінімальний JS лише перемикає data-state. Вміст вкладок — через data-[hidden=true]:hidden. Жодних класів active, hidden, selected — лише data-атрибути та довільні варіанти.
Шпаргалка: всі варіанти в одному місці
| Варіант | Псевдоклас | Коли спрацьовує |
|---|---|---|
hover: | :hover | курсор над елементом |
focus: | :focus | будь-який фокус |
focus-within: | :focus-within | фокус всередині контейнера |
focus-visible: | :focus-visible | фокус через клавіатуру |
active: | :active | під час кліку |
visited: | :visited | відвідане посилання |
disabled: | :disabled | disabled атрибут |
enabled: | :enabled | без disabled |
checked: | :checked | перевірений checkbox/radio |
indeterminate: | :indeterminate | напівперевірений |
required: | :required | required атрибут |
optional: | :optional | без required |
valid: | :valid | валідний input |
invalid: | :invalid | невалідний input |
in-range: | :in-range | число у межах min/max |
out-of-range: | :out-of-range | поза межами |
placeholder-shown: | :placeholder-shown | input порожній (показує placeholder) |
autofill: | :autofill | браузерний автофіл |
read-only: | :read-only | readonly атрибут |
| Варіант | Псевдоклас | Коли спрацьовує |
|---|---|---|
first: | :first-child | перший дочірній |
last: | :last-child | останній дочірній |
only: | :only-child | єдиний дочірній |
odd: | :nth-child(odd) | непарний (1-й, 3-й...) |
even: | :nth-child(even) | парний (2-й, 4-й...) |
nth-{n}: | :nth-child(n) | N-й елемент (v4) |
nth-[expr]: | :nth-child(An+B) | математичний вираз (v4) |
first-of-type: | :first-of-type | перший свого типу |
last-of-type: | :last-of-type | останній свого типу |
only-of-type: | :only-of-type | єдиний свого типу |
empty: | :empty | без дочірніх елементів |
not-{variant}: | :not(...) | заперечення (v4) |
| Варіант | Псевдоелемент | Застосування |
|---|---|---|
before: | ::before | декоративний контент до |
after: | ::after | декоративний контент після |
placeholder: | ::placeholder | підказка в input |
file: | ::file-selector-button | кнопка type=file |
marker: | ::marker | маркер списку |
selection: | ::selection | виділений текст |
first-line: | ::first-line | перший рядок |
first-letter: | ::first-letter | перша літера |
backdrop: | ::backdrop | фон для <dialog> |
group: — позначити батька
group-{variant}: — реагувати на стан group
group/{name}: — іменована група
group-{v}/{name}: — іменований варіант групи
peer: — позначити попередній сусідній
peer-{variant}: — реагувати на стан peer
peer/{name}: — іменований peer
has-[selector]: — батько містить selector (v4)
in-[selector]: — елемент всередині selector (v4)
not-[selector]: — заперечення з селектором (v4)
starting: — @starting-style: поява в DOM (v4)
sm: — min-width: 640px (≥ 40rem)
md: — min-width: 768px (≥ 48rem)
lg: — min-width: 1024px (≥ 64rem)
xl: — min-width: 1280px (≥ 80rem)
2xl: — min-width: 1536px (≥ 96rem)
max-sm: — max-width: 639px (< 40rem) (v4)
max-md: — max-width: 767px (< 48rem) (v4)
max-lg: — max-width: 1023px (< 64rem) (v4)
max-xl: — max-width: 1279px (< 80rem) (v4)
dark: — prefers-color-scheme: dark (або .dark)
print: — @media print
portrait: — орієнтація портретна
landscape: — орієнтація ландшафтна
Завдання для самоперевірки
Завдання 1.1. Декодування класів.
Опишіть словами, що робить кожен клас у наступному HTML:
<ul class="divide-y divide-slate-200">
<li class="flex items-center justify-between py-3 px-4
first:rounded-t-xl last:rounded-b-xl
odd:bg-white even:bg-slate-50
hover:bg-indigo-50 transition-colors">
<input class="sr-only peer" type="checkbox" id="item1" />
<label for="item1"
class="text-sm text-slate-700 peer-checked:text-indigo-600 peer-checked:font-semibold cursor-pointer">
Пункт зі станом
</label>
<button class="opacity-0 group-hover:opacity-100
focus-visible:opacity-100 transition-opacity
not-disabled:hover:bg-slate-100
text-slate-400 rounded p-1">
✕
</button>
</li>
</ul>
Завдання 1.2. Toggle Switch без JavaScript.
Реалізуйте перемикач (toggle switch) виключно через CSS. Вимоги:
<input type="checkbox" class="sr-only peer">— прихований, але функціональний- Трек:
w-11 h-6, сірий в off, синій приpeer-checked: - Бігунок: переміщується через
peer-checked:translate-x-5 - Label: змінює колір при
peer-checked:
Завдання 1.3. Форма з валідацією.
Поле type="email" із:
invalid:border-red-400таinvalid:bg-red-50для невалідногоvalid:border-green-400для валідногоplaceholder:text-slate-400 placeholder:italicдля підказкиfocus:ring-2 focus:ring-indigo-500 focus:outline-noneдля фокусуdisabled:opacity-50 disabled:cursor-not-allowedдля вимкненого стану
Завдання 2.1. Accordion без JavaScript.
Реалізуйте accordion (FAQ) на базі <details> та <summary>:
<details class="group border rounded-xl overflow-hidden">
<summary class="flex items-center justify-between p-4 cursor-pointer
marker:hidden list-none">
<span class="font-semibold text-slate-800">Запитання</span>
<!-- Стрілка обертається при open -->
<span class="group-open:rotate-180 transition-transform">▼</span>
</summary>
<!-- Анімація через grid trick або starting: -->
<div class="px-4 pb-4 text-slate-600 text-sm leading-relaxed">
Відповідь на запитання
</div>
</details>
Зробіть 4 секції. Додайте анімацію відкриття через starting: або Grid height-trick.
Завдання 2.2. Картка з group-hover ефектами.
Картка товару з ефектом при hover:
- Фон картки змінюється через
hover:на батьку - Зображення збільшується
group-hover:scale-105 - Заголовок змінює колір
group-hover:text-indigo-700 - Кнопка «Купити» з'являється з
opacity-0 group-hover:opacity-100 translate-y-2 group-hover:translate-y-0 - Ціна змінює розмір
group-hover:text-lg
Завдання 2.3. Dropdown меню через peer + checkbox.
Кнопка «Меню» відкриває dropdown без JavaScript:
- Прихований
<input type="checkbox" class="peer sr-only"> <label>— візуальна кнопка «☰ Меню»- Dropdown
<div>після label:hidden peer-checked:block - При відкритті — іконка змінюється через
peer-checked:rotate-45або заміна тексту
Завдання 3.1. Розумна форма з has- та starting:.*
Реалізуйте форму пошуку з:
has-[:focus]— весь блок пошуку підсвічується при фокусі inputhas-[input:not(:placeholder-shown)]— показується список результатів (без JS)starting:opacity-0 starting:translate-y-2— результати з'являються анімованоnot-first:border-t— розділювачі між результатами через CSS- Кнопка «Очистити» показується через
has-[input:not(:placeholder-shown)]:flex
Завдання 3.2. Responsive Navigation.
Складний навбар:
- Desktop (
lg:): горизонтальний flex, dropdown приgroup-hover:block - Mobile (до
lg:):hidden, гамбургер-кнопка відкриває черезpeer-checked:flex - Dark mode:
dark:bg-slate-900 dark:text-white dark:border-slate-700 - Активний пункт:
data-[state=active]:text-indigo-600 data-[state=active]:font-semibold has-[.active]:— навбар підсвічується якщо є активний пункт
Завдання 3.3. Data-driven компонент.
Система бейджів без JavaScript (лише атрибути + CSS):
data-[variant=success]:bg-green-100 data-[variant=success]:text-green-800data-[variant=warning]:bg-amber-100 data-[variant=warning]:text-amber-800data-[variant=error]:bg-red-100 data-[variant=error]:text-red-800data-[size=sm]:text-xs data-[size=sm]:px-2data-[size=lg]:text-base data-[size=lg]:px-4 data-[size=lg]:py-1.5
Один клас .badge у CSS. Весь стиль через data-атрибути.
Попередня стаття: Кастомізація теми через @themeНаступна стаття: Типографіка та система кольорів
Кастомізація теми через @theme у Tailwind v4
Повний академічний розбір CSS-first конфігурації Tailwind v4: директива @theme, дизайн-токени, OKLCH, шрифти, breakpoints, spacing, тіні та анімації. Побудова власної дизайн-системи з нуля — крок за кроком.
Типографіка та система кольорів у Tailwind v4
Академічний розбір типографічної та кольорової систем Tailwind CSS v4: OKLCH-палітра, opacity modifiers, font-size шкала, line-height, font-weight, text-balance/pretty, prose плагін та практичні патерни. З живими прикладами та завданнями.