Tailwind

Компоненти та повторюваність: @apply, @utility та патерни

Академічний розбір вирішення проблеми повторюваності у Tailwind CSS v4: компонентний підхід, @apply для виняткових випадків, @utility для кастомних утиліт, @layer base/components, @custom-variant та @plugin. Повний набір реальних компонентів із живими прикладами.

Компоненти та повторюваність: @apply, @utility та патерни

Фундаментальна напруга: утилітарність проти повторюваності

Utility-first підхід Tailwind побудований на фундаментальному принципі: кожен CSS-клас описує одну атомарну властивість, а HTML-елемент отримує всі необхідні стилі через список таких класів безпосередньо в атрибуті class. Це дає разючу локальність стилів — читаючи HTML, ви бачите повну картину зовнішнього вигляду елемента без необхідності переглядати CSS-файли.

Однак у кожного розробника, що починає реальний проєкт, виникає однакове питання. Уявіть кнопку:

<button class="inline-flex items-center justify-center gap-2
               px-5 py-2.5 font-semibold rounded-xl transition-colors
               bg-indigo-600 hover:bg-indigo-700 active:scale-95
               text-white shadow-sm hover:shadow-md">
    Зберегти зміни
</button>

Якщо ця кнопка використовується у 40 місцях додатку — чи потрібно оновлювати 40 HTML-рядків, коли дизайнер попросить змінити rounded-xl на rounded-2xl? Відповідь: ні, але рішення залежить від контексту. Tailwind надає кілька підходів до вирішення цієї напруги, і вибір між ними є одним із ключових архітектурних рішень у вашому проєкті.

Tailwind не пропонує одного «правильного» рішення — це навмисно. Різні контексти вимагають різних підходів. Розуміння всіх варіантів і їхніх компромісів є ознакою зрілого Tailwind-розробника.

Рішення 0: JS/TS компоненти — найкраще у фреймворках

Якщо ви працюєте з React, Vue, Svelte, Angular або будь-яким іншим компонентним фреймворком — первинне і найелегантніше рішення проблеми повторюваності є компонент. Це не специфіка Tailwind — це загальний принцип компонентної архітектури.

Ідея проста: замість того, щоб керувати набором Tailwind-класів у CSS, ви інкапсулюєте їх у функцію або компонент. Кожен виклик компонента генерує правильний HTML із правильними класами, а варіанти (primary, secondary, ghost) визначаються через props:

// React: Button — повна система варіантів в одному компоненті
const buttonVariants = {
    primary:   'bg-indigo-600 hover:bg-indigo-700 text-white shadow-sm hover:shadow-md',
    secondary: 'border-2 border-indigo-600 text-indigo-600 hover:bg-indigo-50',
    ghost:     'text-slate-600 hover:bg-slate-100 hover:text-slate-900',
    danger:    'bg-red-500 hover:bg-red-600 text-white',
}

const buttonSizes = {
    sm: 'px-3 py-1.5 text-xs',
    md: 'px-5 py-2.5 text-sm',
    lg: 'px-7 py-3.5 text-base',
}

function Button({
    children,
    variant = 'primary',
    size = 'md',
    disabled = false,
    className = '',
    ...props
}) {
    return (
        <button
            className={`
                inline-flex items-center justify-center gap-2
                font-semibold rounded-xl transition-all
                disabled:opacity-50 disabled:cursor-not-allowed disabled:pointer-events-none
                ${buttonVariants[variant]}
                ${buttonSizes[size]}
                ${className}
            `}
            disabled={disabled}
            {...props}
        >
            {children}
        </button>
    )
}

// Використання — жодного повторення класів
<Button>Зберегти</Button>
<Button variant="secondary">Скасувати</Button>
<Button variant="ghost" size="sm">Додатково</Button>
<Button variant="danger" disabled>Видалити</Button>
🔒localhost:3000

Аналогічно для Vue (через :class binding), Svelte (через class: директиви) або Angular (через [ngClass]). Один компонент — одне місце для зміни. Щоб оновити rounded-xlrounded-2xl для всіх кнопок, достатньо змінити один рядок.

Для React-проєктів розгляньте бібліотеку class-variance-authority (CVA) — вона надає декларативний API для побудови variant-систем на основі Tailwind-класів: cva('base-classes', { variants: { ... } }). Це значно чистіше, ніж ручні об'єкти.

Якщо ви не використовуєте компонентний фреймворк — розглядайте наступні рішення.


Рішення 1: @apply — перенесення класів у CSS

Директива @apply дозволяє витягти набір Tailwind-класів із HTML у CSS-правило. Це класичне рішення для проєктів без компонентних фреймворків — наприклад, для багатосторінкових сайтів на Laravel Blade, Django templates або статичних генераторах:

/* components.css */
@layer components {
    /* Базова кнопка — спільні стилі для всіх варіантів */
    .btn {
        @apply inline-flex items-center justify-center gap-2
               px-5 py-2.5 font-semibold rounded-xl
               transition-all duration-150
               disabled:opacity-50 disabled:cursor-not-allowed;
    }

    /* Варіанти — модифікатори до базового .btn */
    .btn-primary {
        @apply bg-indigo-600 hover:bg-indigo-700 active:bg-indigo-800
               text-white shadow-sm hover:shadow-md;
    }

    .btn-secondary {
        @apply border-2 border-indigo-600 text-indigo-600
               hover:bg-indigo-50 active:bg-indigo-100;
    }

    .btn-ghost {
        @apply text-slate-600 hover:bg-slate-100 hover:text-slate-900;
    }

    .btn-danger {
        @apply bg-red-500 hover:bg-red-600 text-white;
    }

    /* Розміри */
    .btn-sm { @apply px-3 py-1.5 text-xs rounded-lg; }
    .btn-lg { @apply px-7 py-3.5 text-base rounded-2xl; }
}
<!-- HTML стає чистим і семантичним -->
<button class="btn btn-primary">Зберегти</button>
<button class="btn btn-secondary">Скасувати</button>
<button class="btn btn-ghost btn-sm">Додатково</button>
<button class="btn btn-danger" disabled>Видалити</button>
🔒localhost:3000

Обмеження @apply: варіанти та специфічність

@apply має важливе обмеження: він не рекомендується для складних варіантів. Хоча технічно @apply hover:bg-indigo-700 компілюється, це порушує принцип розділення — варіантна логіка потрапляє у CSS, де її важче помітити та перевизначити.

Правильний підхід: виносьте в @apply лише статичні базові стилі, а інтерактивну логіку залишайте у HTML:

/* ✅ Добре: @apply для статичних базових стилів */
.card {
    @apply bg-white rounded-2xl border border-slate-200 p-6;
    /* Без hover:, dark:, focus: у @apply */
}

/* Стилі для станів — через CSS нативно або у HTML */
.card:hover {
    box-shadow: 0 8px 30px -8px oklch(0.1 0.05 264 / 0.15);
}
<!-- ✅ Добре: hover/dark/group варіанти залишаються у HTML -->
<div class="card hover:shadow-lg dark:bg-slate-800 dark:border-slate-700 transition-shadow">
    ...
</div>
@apply повертає вас до парадигми «CSS-файл із класами» — ви знову пишете іменовані CSS-класи, просто через Tailwind-синтаксис. Переваги Tailwind (локальність стилів у HTML, відсутність прихованих залежностей) частково зникають. Використовуйте @apply виважено: лише для справді повторюваних базових компонентів.

Рішення 2: @utility — кастомні атомарні утиліти (Tailwind v4)

@utility — нова директива Tailwind v4, що дозволяє визначити власний utility-клас першого класу. На відміну від @layer components + @apply, клас, визначений через @utility, поводиться абсолютно так само, як вбудовані утиліти Tailwind: підтримує всі варіанти (hover:, dark:, md:, group-hover:, focus-visible: тощо).

Синтаксис та принцип роботи

/* @utility {ім'я} { CSS-властивості } */
@utility btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
    padding: 0.625rem 1.25rem;  /* px-5 py-2.5 */
    font-weight: 600;
    border-radius: 0.75rem;     /* rounded-xl */
    transition-property: color, background-color, border-color, box-shadow, transform;
    transition-duration: 150ms;
    transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}

@utility card {
    background-color: white;
    border-radius: 1rem;
    border: 1px solid oklch(0.928 0.006 264);
    padding: 1.5rem;
}

@utility input-base {
    width: 100%;
    padding: 0.5rem 0.75rem;
    border: 1px solid oklch(0.88 0.01 270);
    border-radius: 0.5rem;
    font-size: 0.875rem;
    outline: none;
    transition: border-color 150ms, box-shadow 150ms;
}

@utility scrollbar-hide {
    -ms-overflow-style: none;
    scrollbar-width: none;

    &::-webkit-scrollbar {
        display: none;
    }
}

Ключова перевага: повна підтримка варіантів

Це принципова відмінність від @layer components. Клас через @utility є повноправним членом Tailwind-екосистеми:

<!-- @utility btn підтримує ВСІ варіанти — як вбудовані утиліти -->

<!-- Responsive: розмір змінюється за breakpoint -->
<button class="btn md:text-base lg:text-lg">
    Адаптивна кнопка
</button>

<!-- dark: — темний режим -->
<button class="btn bg-indigo-600 dark:bg-indigo-500 text-white">
    Кнопка з темним режимом
</button>

<!-- group-hover: — реакція через group -->
<div class="group">
    <button class="btn group-hover:shadow-lg border border-slate-200">
        З'являється тінь при hover батька
    </button>
</div>

<!-- hover: + active: + focus-visible: -->
<button class="btn bg-indigo-600 text-white
               hover:bg-indigo-700 active:scale-95
               focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:outline-none">
    Повна інтерактивність
</button>

<!-- Стекання: md:dark:hover: -->
<button class="btn md:dark:hover:bg-indigo-400">
    Три умови одночасно
</button>
🔒localhost:3000
@apply у @layer components
Підхід v3
Класи-компоненти через @apply. Варіанти (hover:, dark:) технічно компілюються, але є антипатерном. Специфічність може несподівано перевизначатись.
@utility
Підхід v4 (рекомендований)
Власні утиліти першого класу. Повна підтримка варіантів без обмежень. Поводиться так само, як flex, p-4, text-sm. Правильний сучасний підхід.

🧪 Практика: власна утилітарна система

Визначте через @utility три базові компоненти: btn (кнопка з flex та padding), card (картка з border та rounded), badge (мітка з px/py та rounded-full). Потім використайте їх у HTML, поєднуючи з варіантами: hover:card:shadow-lg, dark:badge:bg-slate-700, md:btn:text-base. Переконайтеся, що варіанти працюють коректно.


@layer base — глобальна основа проєкту

Шар base у Tailwind займає найнижчий рівень специфічності і призначений для глобальних стилів, що повинні застосовуватись до всіх елементів проєкту: скидання браузерних стилів, базова типографіка, глобальні CSS Custom Properties:

@layer base {
    /* CSS-скидання та загальні правила */
    *,
    *::before,
    *::after {
        box-sizing: border-box;
    }

    html {
        scroll-behavior: smooth;
        -webkit-text-size-adjust: 100%;
        /* Плавне масштабування тексту на мобільних */
    }

    body {
        @apply font-sans antialiased;
        /* antialiased: -webkit-font-smoothing: antialiased */
        color: var(--color-text, oklch(0.15 0.02 264));
        background-color: var(--color-bg, oklch(1 0 0));
        line-height: 1.6;
    }

    /* Заголовки: базова ієрархія */
    h1, h2, h3, h4, h5, h6 {
        font-weight: 700;
        line-height: 1.2;
        @apply text-slate-900 tracking-tight;
    }

    h1 { @apply text-4xl; }
    h2 { @apply text-3xl; }
    h3 { @apply text-2xl; }
    h4 { @apply text-xl; }

    /* Посилання: глобальний стиль */
    a {
        @apply text-indigo-600 transition-colors;
        text-underline-offset: 3px;
    }
    a:hover { @apply text-indigo-700; }

    /* Зображення: не виходять за межі контейнера */
    img, video, svg {
        max-width: 100%;
        display: block;
    }

    /* Accessibility: focus-visible для клавіатурної навігації */
    :focus-visible {
        outline: 2px solid oklch(0.58 0.22 277);
        outline-offset: 2px;
        border-radius: 2px;
    }

    /* Виділення тексту: брендовий колір */
    ::selection {
        background-color: oklch(0.58 0.22 277 / 0.2);
        color: oklch(0.2 0.05 264);
    }
}
@layer base конкурує з Tailwind Preflight — вбудованим скиданням стилів, яке автоматично підключається через @import 'tailwindcss'. Preflight базується на modern-normalize і вже скидає більшість браузерних стилів. У @layer base додавайте лише те, чого Preflight не охоплює.

@layer components — бібліотека класів-компонентів

Шар components призначений для семантичних класів, що поєднують кілька CSS-властивостей та представляють концептуальний компонент UI. Він розташований між base (нижче) і utilities (вище), що дозволяє утилітарним класам перевизначати компонентні:

@layer components {

    /* ─── Badge System ─────────────────────────────────── */
    .badge {
        @apply inline-flex items-center gap-1
               px-2.5 py-0.5
               text-xs font-semibold rounded-full;
    }

    .badge-success  { @apply bg-emerald-100 text-emerald-800; }
    .badge-warning  { @apply bg-amber-100   text-amber-800;   }
    .badge-error    { @apply bg-red-100     text-red-800;     }
    .badge-info     { @apply bg-sky-100     text-sky-800;     }
    .badge-neutral  { @apply bg-slate-100   text-slate-700;   }
    .badge-purple   { @apply bg-violet-100  text-violet-800;  }

    /* ─── Form System ───────────────────────────────────── */
    .form-group  { @apply flex flex-col gap-1.5; }
    .form-label  { @apply text-sm font-semibold text-slate-700; }
    .form-hint   { @apply text-xs text-slate-400; }
    .form-error  { @apply text-xs text-red-500 flex items-center gap-1; }

    .form-input {
        @apply w-full px-3 py-2
               border border-slate-300 rounded-lg
               text-sm text-slate-900 placeholder:text-slate-400
               focus:outline-none focus:border-indigo-500
               focus:ring-2 focus:ring-indigo-500/20
               transition-colors;
    }

    .form-input-error {
        @apply border-red-400 bg-red-50/50
               focus:border-red-500 focus:ring-red-500/20;
    }

    .form-textarea {
        @apply form-input resize-none min-h-24;
    }

    /* ─── Alert System ──────────────────────────────────── */
    .alert {
        @apply flex gap-3 p-4 rounded-xl border text-sm;
    }

    .alert-success {
        @apply bg-emerald-50 border-emerald-200 text-emerald-800;
    }

    .alert-warning {
        @apply bg-amber-50 border-amber-200 text-amber-800;
    }

    .alert-error {
        @apply bg-red-50 border-red-200 text-red-800;
    }

    .alert-info {
        @apply bg-sky-50 border-sky-200 text-sky-800;
    }

    /* ─── Divider ───────────────────────────────────────── */
    .divider {
        @apply flex items-center gap-3
               text-xs text-slate-400 uppercase tracking-widest
               before:flex-1 before:h-px before:bg-slate-200
               after:flex-1  after:h-px  after:bg-slate-200;
    }
}

Перевага @layer components: класи-компоненти перевизначаються утилітарними класами через механізм шарування. Якщо вам потрібно для конкретного <input> задати інший border-radius — просто додайте rounded-2xl у class="", і воно переможе .form-input:

<!-- .form-input: rounded-lg -->
<!-- rounded-2xl перевизначає rounded-lg з components шару -->
<input class="form-input rounded-2xl" />

🧪 Практика: форма через @layer components

Визначте у @layer components повну форм-систему: .form-group, .form-label, .form-input, .form-input-error, .form-hint, .form-error. Реалізуйте форму реєстрації з трьома полями (ім'я, email, password), кнопкою submit та divider. Один із inputs — у стані error. Жодних довгих ланцюжків класів у HTML — тільки семантичні класи.


Реальні компоненти: повний demo

Набір компонентів, що демонструє комбінацію всіх підходів: базові стилі через @layer components, інтерактивна поведінка через Tailwind-варіанти у HTML.

🔒localhost:3000

@custom-variant: власні варіанти без JavaScript

Концепція та мотивація

Система варіантів Tailwind (hover:, dark:, md:, focus:) є декларативним відображенням CSS-умов: кожен варіант — це умова, за якої застосовується утилітарний клас. Логічне питання: а чи можна визначати власні умови? До Tailwind v4 відповідь потребувала JavaScript-плагіну:

// v3: JS-плагін — єдиний спосіб додати власний варіант
plugin(function({ addVariant }) {
    addVariant('hocus', ['&:hover', '&:focus'])
    addVariant('dark',  '.dark &')
    addVariant('theme-ocean', '.theme-ocean &')
})

Tailwind v4 вирішує це елегантно — через директиву @custom-variant у чистому CSS:

/* v4: @custom-variant — без JS */
@custom-variant hocus (&:hover, &:focus-visible);
@custom-variant dark  (&:is(.dark *));
@custom-variant theme-ocean (&:is(.theme-ocean *));

Синтаксис: однорядковий та блоковий

@custom-variant має два варіанти синтаксису:

Однорядковий — для умов на основі CSS-селекторів:

/* @custom-variant {name} ({CSS-умова}); */

/* Поєднання станів: hover або focus */
@custom-variant hocus (&:hover, &:focus-visible);

/* Контекст батьківського класу */
@custom-variant dark        (&:is(.dark *));
@custom-variant theme-ocean (&:is(.theme-ocean *));
@custom-variant theme-forest (&:is(.theme-forest *));

/* data-атрибути */
@custom-variant data-active  (&[data-active]);
@custom-variant data-loading (&[data-loading]);
@custom-variant data-error   (&[data-error="true"]);

/* aria-атрибути */
@custom-variant aria-expanded (&[aria-expanded="true"]);
@custom-variant aria-selected (&[aria-selected="true"]);

Блоковий — для медіазапитів та @supports:

/* @custom-variant {name} { @{media-or-rule} { @slot; } } */

/* Стилі для друку */
@custom-variant print {
    @media print {
        @slot;
        /* @slot — місце підстановки CSS-правила */
    }
}

/* Анімації лише для користувачів без обмежень */
@custom-variant motion-ok {
    @media (prefers-reduced-motion: no-preference) {
        @slot;
    }
}

/* Ландшафтна орієнтація */
@custom-variant landscape {
    @media (orientation: landscape) {
        @slot;
    }
}

/* CSS Grid підтримка */
@custom-variant supports-grid {
    @supports (display: grid) {
        @slot;
    }
}

Практичні приклади

Приклад 1: Dark mode через клас .dark на <html>

Стандартна конфігурація Tailwind використовує prefers-color-scheme: dark для dark:. Якщо ви хочете керувати темою програмно — через клас на <html>:

/* main.css */
@import 'tailwindcss';

/* Перевизначити вбудований dark: варіант */
@custom-variant dark (&:is(.dark *));
<html class="dark"> <!-- або без класу для light -->
    <body>
        <div class="bg-white dark:bg-slate-900 text-slate-900 dark:text-white
                    transition-colors duration-300">
            Автоматична адаптація до теми
        </div>
    </body>
</html>
// Перемикання теми через JS
document.documentElement.classList.toggle('dark')

Приклад 2: Multi-theme система

@import 'tailwindcss';

@custom-variant theme-ocean  (&:is(.theme-ocean *));
@custom-variant theme-forest (&:is(.theme-forest *));
@custom-variant theme-sunset (&:is(.theme-sunset *));
<!-- Зміна теми через клас на html або будь-якому предку -->
<html class="theme-forest">
    <div class="bg-slate-50
                theme-ocean:bg-cyan-50
                theme-forest:bg-emerald-50
                theme-sunset:bg-orange-50

                text-slate-900
                theme-ocean:text-cyan-900
                theme-forest:text-emerald-900
                theme-sunset:text-amber-900

                transition-colors">
        Контент змінює тему відповідно до класу на html
    </div>
</html>

Приклад 3: hocus — hover або focus-visible

Один із найпопулярніших UX-патернів: елемент має однакову реакцію на мишу та клавіатуру. Без hocus потрібно дублювати класи:

<!-- До: дублювання для hover і focus-visible -->
<a class="text-slate-700 hover:text-indigo-600 focus-visible:text-indigo-600
          hover:underline focus-visible:underline
          hover:underline-offset-4 focus-visible:underline-offset-4 transition-colors">
    Посилання
</a>

<!-- Після: @custom-variant hocus (&:hover, &:focus-visible) -->
<a class="text-slate-700 hocus:text-indigo-600 hocus:underline hocus:underline-offset-4 transition-colors">
    Посилання
</a>

Приклад 4: data-* стани компонентів

Найважливіший практичний патерн: замість перемикання CSS-класів через JavaScript — керування через data-атрибути, що відображаються на CSS:

@custom-variant data-active  (&[data-active]);
@custom-variant data-loading (&[data-loading]);
@custom-variant data-error   (&[data-error]);
<!-- Tab-система: JavaScript лише перемикає data-active -->
<div class="flex p-1 bg-slate-100 rounded-xl gap-1" id="tabs">
    <button data-active
            class="flex-1 py-2 rounded-lg text-sm font-semibold
                   text-slate-500 data-active:bg-white data-active:text-indigo-600
                   data-active:shadow-sm transition-all">
        Головна
    </button>
    <button class="flex-1 py-2 rounded-lg text-sm font-semibold
                   text-slate-500 data-active:bg-white data-active:text-indigo-600
                   data-active:shadow-sm transition-all">
        Аналітика
    </button>
</div>

<!-- Loading state кнопка -->
<button data-loading
        class="px-5 py-2.5 bg-indigo-600 text-white font-semibold rounded-xl
               data-loading:opacity-70 data-loading:cursor-wait
               data-loading:pointer-events-none transition-all">
    <span class="data-loading:hidden">Надіслати</span>
    <span class="hidden data-loading:inline-flex items-center gap-2">
        ⟳ Завантаження...
    </span>
</button>

<!-- Input з error state -->
<input data-error
       class="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm
              data-error:border-red-400 data-error:bg-red-50/50
              focus:outline-none focus:ring-2
              data-error:focus:ring-red-500/20 transition-colors">

Приклад 5: print — стилі для друку

@custom-variant print {
    @media print {
        @slot;
    }
}
<!-- Приховати UI-елементи при друку -->
<nav class="print:hidden">Навігація (не друкується)</nav>
<button class="print:hidden">Підписатись</button>

<!-- Адаптувати розміри для друку -->
<h1 class="text-4xl print:text-2xl">Заголовок</h1>
<p class="text-base print:text-sm print:leading-tight">Основний текст</p>

<!-- Прибрати декоративні ефекти -->
<div class="shadow-2xl print:shadow-none bg-gradient-to-br print:bg-white from-indigo-50">
    Контент
</div>

<!-- Показати URL посилань при друку через after: -->
<a class="text-indigo-600 print:text-slate-900 print:after:content-['_(' attr(href) ')']"
   href="https://example.com">
    Посилання
</a>
🔒localhost:3000

🧪 Практика: власна тема-система

Реалізуйте повноцінну multi-theme систему через @custom-variant. Визначте три теми: theme-ocean (cyan/teal палітра), theme-forest (emerald/green) та theme-sunset (orange/amber). Кожна тема — власний @custom-variant. Реалізуйте картку продукту, що автоматично змінює кольори фону, акцентів та тексту залежно від активної теми. Додайте кнопки перемикання теми через JS, що ставлять клас на <html>.


@plugin — плагіни Tailwind v4

Офіційні плагіни через @plugin

Tailwind v4 змінює механізм підключення плагінів: замість конфігурації у tailwind.config.js — директива @plugin у CSS:

/* main.css */
@import 'tailwindcss';

/* Офіційні плагіни */
@plugin "@tailwindcss/typography";    /* prose, prose-lg, prose-invert... */
@plugin "@tailwindcss/forms";         /* Покращені стилі форм */
@plugin "@tailwindcss/aspect-ratio";  /* aspect-w-*, aspect-h-* */
# Встановлення плагінів
npm install @tailwindcss/typography @tailwindcss/forms --save-dev

Після підключення @tailwindcss/typography клас prose стає доступним з повним набором варіантів:

<!-- @tailwindcss/typography: стилізація довільного HTML -->
<article class="prose prose-lg prose-indigo dark:prose-invert max-w-prose mx-auto">
    <h1>Заголовок статті</h1>
    <p>Перший параграф із <strong>жирним</strong> та <a href="#">посиланням</a>.</p>
    <blockquote>Цитата з автоматичним стилем</blockquote>
    <pre><code>const x = 1; // code block</code></pre>
</article>

JS-плагін: складна логіка

Якщо @custom-variant та @utility не покривають потреби — можна писати JS-плагін:

/* main.css */
@plugin "./plugins/custom-utilities.js";
// plugins/custom-utilities.js
import plugin from 'tailwindcss/plugin'

export default plugin(function ({ addUtilities, addVariant, matchUtilities, theme }) {
    // addUtilities: статичні утиліти
    addUtilities({
        '.scrollbar-hide': {
            '-ms-overflow-style': 'none',
            'scrollbar-width': 'none',
            '&::-webkit-scrollbar': { display: 'none' },
        },
        '.font-smoothing': {
            '-webkit-font-smoothing': 'antialiased',
            '-moz-osx-font-smoothing': 'grayscale',
        },
        '.text-balance': { 'text-wrap': 'balance' },
        '.text-pretty':  { 'text-wrap': 'pretty' },
    })

    // matchUtilities: динамічні утиліти з arbitrary values
    matchUtilities(
        {
            'grid-cols-fill': (value) => ({
                'grid-template-columns': `repeat(auto-fill, minmax(${value}, 1fr))`,
            }),
        },
        { values: theme('spacing') }
    )

    // addVariant: кастомний варіант (краще через @custom-variant у CSS)
    addVariant('hocus', ['&:hover', '&:focus-visible'])
})
<!-- Використання JS-плагіну -->
<div class="overflow-x-auto scrollbar-hide">Прокрутка без скролбару</div>
<h1 class="font-smoothing">Згладжений шрифт</h1>
<div class="grid grid-cols-fill-[250px] gap-4">Автоматична сітка</div>
<a class="hocus:text-indigo-600">Hover або focus</a>
Таблиця вибору інструменту:
ІнструментПризначенняКоли використовувати
@utilityАтомарна утиліта з CSSВласні одно-властивісні класи: scrollbar-hide, font-smoothing
@layer components + @applyСемантичний клас-компонентПовторювані патерни у шаблонах без фреймворку
@custom-variantВласна CSS-умоваНові стани: data-*, aria-*, print, multi-theme
JS @pluginСкладна логікаДинамічні утиліти, обчислювані значення, matchUtilities
JS компонентІнкапсуляціяReact/Vue/Svelte — завжди перший вибір

🧪 Практика: власний плагін

Напишіть JS-плагін, що додає три утиліти: (1) .text-balance і .text-pretty як статичні утиліти, (2) .grid-auto-fill-{spacing} як динамічну утиліту через matchUtilities (значення з theme('spacing')), (3) @custom-variant (або addVariant) hocus. Використайте цей плагін у демо-сторінці із сіткою карток, збалансованими заголовками та hocus: посиланнями.


Архітектура CSS файлів: production-ready структура

Для production-проєктів CSS-файли організовуються за принципом відповідальності:

src/styles/
├── main.css           ← Точка входу: @import усього
├── theme/
│   ├── colors.css     ← @theme { --color-* }
│   ├── fonts.css      ← @theme { --font-* } + @font-face
│   └── tokens.css     ← @theme { --spacing-*, --radius-*, --shadow-* }
├── base/
│   ├── reset.css      ← @layer base { *, html, body }
│   └── globals.css    ← @layer base { h*, a, ::selection, :focus-visible }
├── components/
│   ├── buttons.css    ← @utility btn + @layer components .btn-*
│   ├── forms.css      ← @layer components .form-*
│   ├── cards.css      ← @layer components .card*
│   └── badges.css     ← @layer components .badge*
├── variants/
│   └── custom.css     ← @custom-variant dark, hocus, print...
└── utilities/
    └── custom.css     ← @utility scrollbar-hide, font-smoothing...
/* main.css — точка входу */

/* 1. Зовнішні шрифти (до @import tailwindcss!) */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap');

/* 2. Tailwind */
@import 'tailwindcss';

/* 3. Плагіни */
@plugin "@tailwindcss/typography";
@plugin "@tailwindcss/forms";

/* 4. Дизайн-токени */
@import './theme/colors.css';
@import './theme/fonts.css';
@import './theme/tokens.css';

/* 5. Кастомні варіанти */
@import './variants/custom.css';

/* 6. Base шар */
@import './base/reset.css';
@import './base/globals.css';

/* 7. Компоненти */
@import './components/buttons.css';
@import './components/forms.css';
@import './components/cards.css';
@import './components/badges.css';

/* 8. Кастомні утиліти */
@import './utilities/custom.css';

Ця структура забезпечує:

  • Передбачуваний порядок: Tailwind Preflight → base → components → utilities
  • Ізоляцію: кожен файл — одна відповідальність
  • Масштабованість: нові компоненти — нові файли, без конфліктів

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


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

Copyright © 2026