Tailwind

Варіанти: hover, focus, responsive, dark mode та нові v4

Система варіантів Tailwind CSS v4: стани взаємодії, structural pseudo-classes, group та peer, нові варіанти has-*, not-*, nth-*, starting:, адаптивні breakpoints та dark mode.

Варіанти: 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-порядку).

Preview
×
🔒localhost:3000

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>

Шпаргалка: всі варіанти в одному місці


Завдання для самоперевірки


Попередня стаття: Кастомізація теми через @themeНаступна стаття: Типографіка та система кольорів

Copyright © 2026