Tailwind

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

Вирішення проблеми повторюваності в Tailwind: @apply для виняткових випадків, @utility для кастомних утиліт v4, @layer components для компонентних класів. Реальні компоненти: Button, Input, Card без JS-фреймворку.

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

Проблема, яка виникає у кожного

Щойно ви освоїли Tailwind і починаєте будувати реальний проєкт — з'являється питання:

У мене є кнопка: 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 text-white. Вона використовується 40 разів. Якщо дизайнер скаже змінити rounded-xl на rounded-2xl — мені оновлювати 40 місць?

Це реальна проблема. І Tailwind має кілька рішень — але кожне підходить для свого контексту.


Рішення 0: Компоненти (найкраще у фреймворках)

Якщо ви використовуєте React, Vue, Svelte, Angular — перше і краще рішення: компонент.

// React: кнопка — це компонент, а не CSS
function Button({ children, variant = 'primary', ...props }) {
    const variants = {
        primary: 'bg-indigo-600 hover:bg-indigo-700 text-white',
        secondary: 'border-2 border-indigo-600 text-indigo-600 hover:bg-indigo-50',
        ghost: 'text-slate-600 hover:bg-slate-100',
    }
    return (
        <button
            className={`inline-flex items-center justify-center gap-2 px-5 py-2.5 font-semibold rounded-xl transition-colors ${variants[variant]}`}
            {...props}
        >
            {children}
        </button>
    )
}

// Використання — без повторення класів
<Button>Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="ghost">Ghost</Button>

Один компонент — одне місце для зміни. @apply не потрібний. Ось чому Tailwind так популярний саме в React/Vue-екосистемі.


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

Якщо ви не використовуєте компонентні фреймворки — @apply дозволяє витягти набір Tailwind-класів у CSS-правило:

/* components.css */
@layer components {
    .btn {
        @apply inline-flex items-center justify-center gap-2
               px-5 py-2.5 font-semibold rounded-xl transition-colors;
    }

    .btn-primary {
        @apply bg-indigo-600 hover:bg-indigo-700 text-white;
    }

    .btn-secondary {
        @apply border-2 border-indigo-600 text-indigo-600 hover:bg-indigo-50;
    }
}
<!-- Тепер HTML чистий -->
<button class="btn btn-primary">Primary</button>
<button class="btn btn-secondary">Secondary</button>

Обмеження @apply

@apply не підтримує варіанти повністю. Запис @apply hover:bg-indigo-700 — компілюється, але рекомендується уникати. Краще виносити тільки базові стилі:

/* Добре: базові стилі в @apply */
.card {
    @apply bg-white rounded-xl border border-slate-200 shadow-sm p-6;
}

/* Уникайте: варіанти в @apply — краще в HTML */
/* hover:, focus:, dark:, responsive: → залишайте у class="" */

Коли НЕ варто @apply

@apply — це компроміс. Ви повертаєтесь до "CSS-файлу з класами", але тепер вони написані через Tailwind-синтаксис. Всі переваги Tailwind (читабельність у HTML, локальність стилів) частково зникають.Використовуйте @apply тільки для реально повторюваних базових патернів: кнопки, inputs, картки. Не застосовуйте до unікальних секцій сторінки.

Рішення 2: @utility — кастомний utility-клас (НОВА v4)

@utility — нова директива Tailwind v4, що дозволяє визначити власний utility-клас зі всіма можливостями варіантів (hover:, dark:, responsive:, group-hover: тощо).

/* Ваш utility-клас, що поводиться як вбудований Tailwind-клас */
@utility btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
    padding: 0.625rem 1.25rem;
    font-weight: 600;
    border-radius: 0.75rem;
    transition-property: color, background-color, border-color;
    transition-duration: 150ms;
}

@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;
}

Ключова різниця від @apply: з @utility варіанти працюють:

<!-- @utility btn підтримує всі варіанти: hover:, dark:, group-hover:, md: -->
<button class="btn hover:scale-[1.02] bg-indigo-600 text-white dark:bg-indigo-500">
    <button class="btn group-hover:shadow-lg border border-slate-300"></button>
</button>

З @layer components (старий підхід) варіанти з кастомними класами не завжди поводяться коректно. @utility — правильний moderne підхід.


@layer base — глобальні стилі

@layer base — найнижчий рівень: скидання браузерних стилів та глобальні налаштування:

@layer base {
    *,
    *::before,
    *::after {
        box-sizing: border-box;
    }

    html {
        scroll-behavior: smooth;
        -webkit-text-size-adjust: 100%;
    }

    body {
        @apply font-sans antialiased text-slate-700 bg-white;
        line-height: 1.6;
    }

    h1,
    h2,
    h3,
    h4,
    h5,
    h6 {
        font-weight: 700;
        line-height: 1.2;
        @apply text-slate-900;
    }

    a {
        @apply text-indigo-600 hover:text-indigo-700 transition-colors;
    }

    img {
        max-width: 100%;
        display: block;
    }

    /* Focus visible — доступність */
    :focus-visible {
        outline: 2px solid oklch(0.6 0.22 270);
        outline-offset: 2px;
    }
}

@layer components — компонентні класи

Для класів, що поєднують несколько стилів і представляють концептуальні компоненти:

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

    /* Semantic badge variants */
    .badge-success {
        @apply bg-emerald-100 text-emerald-700;
    }
    .badge-warning {
        @apply bg-amber-100 text-amber-700;
    }
    .badge-error {
        @apply bg-red-100 text-red-700;
    }
    .badge-info {
        @apply bg-sky-100 text-sky-700;
    }

    /* Input combo: label + input + hint */
    .form-group {
        @apply flex flex-col gap-1.5;
    }
    .form-label {
        @apply text-sm font-semibold text-slate-700;
    }
    .form-input {
        @apply 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;
    }
    .form-hint {
        @apply text-xs text-slate-400;
    }
    .form-error {
        @apply text-xs text-red-500;
    }

    /* 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, @utility та inline Tailwind:

Preview
×
🔒localhost:3000

@plugin — власний плагін (v4)

Для повторюваної логіки на рівні всього проєкту — плагін:

/* main.css */
@import 'tailwindcss';
@plugin "./plugins/forms.js";
// plugins/forms.js
import plugin from 'tailwindcss/plugin'

export default plugin(function ({ addComponents, addUtilities, theme }) {
    // Компоненти
    addComponents({
        '.card': {
            backgroundColor: 'white',
            borderRadius: theme('borderRadius.xl'),
            border: `1px solid ${theme('colors.slate.200')}`,
            boxShadow: theme('boxShadow.sm'),
            padding: theme('spacing.6'),
        },
    })

    // Утиліти з варіантами
    addUtilities({
        '.scrollbar-hide': {
            '-ms-overflow-style': 'none',
            'scrollbar-width': 'none',
            '&::-webkit-scrollbar': { display: 'none' },
        },
    })
})

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


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

Copyright © 2026