Кастомізація теми через @theme у Tailwind v4
Кастомізація теми через @theme у Tailwind v4
CSS-first: конфігурація, що живе у CSS
У Tailwind v4 відбулася революція у підході до конфігурації. Більше ніякого JavaScript-файлу для налаштувань. Тепер усе — у CSS.
Чому це краще? Тому що конфігурація CSS-фреймворку має бути у CSS. Коли ви відкриваєте проєкт і хочете зрозуміти його дизайн-систему — ви відкриваєте CSS-файл, а не шукаєте tailwind.config.js. Це природніше і значно менш "магічно".
Ключова директива нового підходу — @theme.
@theme — оголошення дизайн-токенів
@theme директива — це блок у вашому CSS-файлі, де ви оголошуєте дизайн-токени. Токен — це іменована одиниця вашої дизайн-системи: конкретний колір, шрифт, відступ, брейкпоінт.
Кожен токен у @theme автоматично генерує відповідні utility-класи.
@import 'tailwindcss';
@theme {
/* Кожна CSS-змінна тут → відповідний utility-клас */
--color-brand: oklch(0.6 0.2 270);
/* Генерує: bg-brand, text-brand, border-brand, ... */
--font-display: 'Satoshi', system-ui, sans-serif;
/* Генерує: font-display */
--breakpoint-3xl: 120rem;
/* Генерує: 3xl: варіант */
}
Магія в тому, що Tailwind розпізнає неймспейс (--color-*, --font-*, --breakpoint-*) і знає, які утиліти генерувати.
Неймспейси @theme
Tailwind розпізнає такі неймспейси CSS-змінних і генерує для них утиліти:
| CSS-змінна (неймспейс) | Що генерується | Приклад утиліти |
|---|---|---|
--color-{name} | bg-, text-, border-, fill-, stroke-, ... | bg-brand, text-primary |
--font-{name} | font-{name} | font-sans, font-display |
--text-{size} | text-{size} | text-hero |
--breakpoint-{name} | {name}: variant | 3xl:flex |
--spacing | Вся spacing-шкала | p-4, m-6, gap-3 |
--radius-{name} | rounded-{name} | rounded-brand |
--shadow-{name} | shadow-{name} | shadow-card |
--animate-{name} | animate-{name} | animate-wiggle |
--ease-{name} | ease-{name} у transition | ease-fluid |
--blur-{name} | blur-{name} | blur-heavy |
Кастомні кольори
Чому OKLCH?
Tailwind v4 перейшов на OKLCH (OKLab Lightness Chroma Hue) для вбудованої палітри. Розберемо, чому:
- Перцептуальна рівномірність: зміна
Lна 10% дає однаковий візуальний стрибок незалежно від відтінку. У sRGB це непередбачувано. - Ширший кольоровий охват: P3 та Rec2020 дисплеї (Apple Retina) показують кольори яскравіше, ніж sRGB дозволяє. OKLCH охоплює і ці кольори.
- Природне затемнення/освітлення:
oklch(0.8 0.15 270)→ зменш L до0.6→ природне затемнення без зсуву відтінку.
OKLCH складається з трьох компонентів: oklch(L C H):
- L (Lightness): від
0(чорний) до1(білий) - C (Chroma): насиченість, від
0(сірий) до~0.4(максимум) - H (Hue): відтінок від
0до360(як у HSL)
oklch(0.6 0.2 270)
/* L=0.6 приблизно середня яскравість */
/* C=0.2 помірна насиченість */
/* H=270 фіолетовий/індиго */
Визначення кастомних кольорів
@theme {
/* Простий бренд-колір */
--color-brand: oklch(0.6 0.2 270);
/* Повна палітра бренду (50-950) */
--color-brand-50: oklch(0.97 0.03 270);
--color-brand-100: oklch(0.93 0.06 270);
--color-brand-200: oklch(0.88 0.1 270);
--color-brand-300: oklch(0.8 0.14 270);
--color-brand-400: oklch(0.71 0.18 270);
--color-brand-500: oklch(0.6 0.22 270);
--color-brand-600: oklch(0.52 0.22 270);
--color-brand-700: oklch(0.43 0.2 270);
--color-brand-800: oklch(0.34 0.17 270);
--color-brand-900: oklch(0.26 0.13 270);
--color-brand-950: oklch(0.17 0.09 270);
}
Тепер у вашому HTML доступні: bg-brand-500, text-brand-600, border-brand-200, ring-brand-300 тощо.
Скидання дефолтних кольорів
Якщо хочете тільки ваші кольори без вбудованої палітри Tailwind:
@theme {
/* Видалити всі дефолтні кольори */
--color-*: initial;
/* Визначити тільки свої */
--color-white: #ffffff;
--color-black: #000000;
--color-primary: oklch(0.6 0.22 270);
--color-secondary: oklch(0.55 0.18 180);
--color-accent: oklch(0.72 0.19 50);
--color-neutral: oklch(0.5 0.01 270);
--color-success: oklch(0.65 0.18 145);
--color-warning: oklch(0.8 0.18 85);
--color-error: oklch(0.6 0.22 25);
}
Це — мінімалістична дизайн-система з одним набором семантичних кольорів.
Семантичні токени: найкраща практика
Замість прив'язки до конкретного відтінку — семантичне іменування:
@theme {
/* Базова палітра */
--color-indigo-500: oklch(0.585 0.233 277.117);
--color-indigo-600: oklch(0.511 0.262 276.966);
--color-slate-900: oklch(0.129 0.042 264.695);
--color-slate-500: oklch(0.554 0.046 257.417);
/* Семантичні аліаси */
--color-bg-base: oklch(1 0 0); /* white */
--color-bg-surface: oklch(0.976 0.002 247); /* slate-50 */
--color-text-primary: oklch(0.129 0.042 264); /* slate-900 */
--color-text-muted: oklch(0.554 0.046 257); /* slate-500 */
--color-accent: oklch(0.585 0.233 277); /* indigo-500 */
--color-accent-dark: oklch(0.511 0.262 277); /* indigo-600 */
}
Тепер ваш HTML: bg-bg-base, text-text-primary, text-text-muted, bg-accent, hover:bg-accent-dark. Набагато зрозуміліше, ніж bg-white text-slate-900 text-slate-500 bg-indigo-500 hover:bg-indigo-600.
Кастомні шрифти
@theme {
/* Sans — основний шрифт */
--font-sans: 'Inter', system-ui, -apple-system, sans-serif;
/* Display — для заголовків */
--font-display: 'Clash Display', 'Inter', sans-serif;
/* Mono — для коду */
--font-mono: 'JetBrains Mono', 'Fira Code', monospace;
}
Для завантаження Google Fonts — @import перед @theme:
/* Важливо: @import ПЕРЕД @theme та @import "tailwindcss" */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
@import 'tailwindcss';
@theme {
--font-sans: 'Inter', system-ui, sans-serif;
}
Тепер font-sans використовуватиме Inter, а не системний шрифт.
Кастомні breakpoints
@theme {
/* Додати новий breakpoint (без зміни існуючих) */
--breakpoint-xs: 20rem; /* 320px */
--breakpoint-3xl: 120rem; /* 1920px для ultra-wide */
--breakpoint-4xl: 160rem; /* 2560px */
}
Використання: xs:flex, 3xl:grid-cols-6.
Щоб перевизначити існуючі breakpoints:
@theme {
--breakpoint-*: initial; /* Скинути всі */
--breakpoint-sm: 36rem; /* 576px */
--breakpoint-md: 48rem; /* 768px */
--breakpoint-lg: 64rem; /* 1024px */
--breakpoint-xl: 80rem; /* 1280px */
}
Кастомний spacing
Найпотужніша змінна: одна --spacing визначає базову одиницю для ВСІЄЇ spacing-системи.
@theme {
--spacing: 0.25rem; /* default — 4px */
/* p-1 = 1 × 0.25rem = 4px */
/* p-4 = 4 × 0.25rem = 16px */
/* p-8 = 8 × 0.25rem = 32px */
}
/* Якщо хочете компактніший UI: */
@theme {
--spacing: 0.2rem; /* 3.2px — щільніший */
}
/* Або просторіший: */
@theme {
--spacing: 0.3rem; /* 4.8px — ширший */
}
Також можна додавати конкретні spacing-значення:
@theme {
--spacing-18: 4.5rem; /* p-18, m-18, gap-18 */
--spacing-22: 5.5rem; /* p-22, m-22, gap-22 */
--spacing-128: 32rem; /* p-128 — дуже великий відступ */
}
Кастомні радіуси, тіні та анімації
Border Radius
@theme {
--radius-brand: 12px; /* rounded-brand */
--radius-xl: 1rem; /* Перевизначення існуючого xl */
--radius-blob: 30% 70% 70% 30% / 30% 30% 70% 70%; /* Складна форма */
}
Box Shadow
@theme {
--shadow-card: 0 2px 8px rgba(0, 0, 0, 0.08), 0 0 1px rgba(0, 0, 0, 0.06);
--shadow-elevated: 0 8px 24px rgba(0, 0, 0, 0.12), 0 2px 8px rgba(0, 0, 0, 0.06);
--shadow-colored: 0 8px 24px oklch(from var(--color-brand) l c h / 0.3);
}
Анімації та keyframes
У @theme можна визначати анімації прямо з keyframes:
@theme {
/* Анімація + keyframes разом */
--animate-fade-in: fade-in 0.3s ease-out both;
--animate-slide-up: slide-up 0.4s ease-out both;
--animate-wiggle: wiggle 1s ease-in-out infinite;
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slide-up {
from {
opacity: 0;
transform: translateY(12px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes wiggle {
0%,
100% {
transform: rotate(-3deg);
}
50% {
transform: rotate(3deg);
}
}
}
Тепер: animate-fade-in, animate-slide-up, animate-wiggle — готові utility-класи.
@theme inline — без CSS-змінних
За замовчуванням @theme генерує CSS Custom Properties у :root і ці змінні доступні у вашому CSS. Але якщо ви не плануєте використовувати їх напряму — можна уникнути цього через @theme inline:
@theme inline {
/* Генерує utility-класи, але НЕ додає CSS-змінні у :root */
--color-secret: oklch(0.6 0.2 270);
}
Використовуйте @theme inline для token-значень, що використовуються тільки через Tailwind-класи, без прямого звернення var(--color-secret).
Живий приклад: повна дизайн-система
<style>
/* Симуляція @theme через CSS-змінні */
:root {
/* Кольорова палітра */
--color-brand-50: oklch(0.97 0.03 290);
--color-brand-100: oklch(0.92 0.07 288);
--color-brand-200: oklch(0.86 0.12 286);
--color-brand-300: oklch(0.76 0.17 284);
--color-brand-400: oklch(0.66 0.22 280);
--color-brand-500: oklch(0.56 0.26 277);
--color-brand-600: oklch(0.48 0.26 275);
--color-brand-700: oklch(0.4 0.23 272);
--color-brand-800: oklch(0.32 0.19 270);
--color-brand-900: oklch(0.24 0.14 268);
--color-success: oklch(0.65 0.18 145);
--color-warning: oklch(0.8 0.18 85);
--color-error: oklch(0.62 0.22 25);
/* Semantic tokens */
--color-bg: oklch(0.99 0 0);
--color-text: oklch(0.15 0.02 270);
--color-muted: oklch(0.55 0.02 270);
--color-border: oklch(0.9 0.01 270);
}
</style>
<div
style="font-family: system-ui, sans-serif; background: var(--color-bg); padding: 1.5rem; border-radius: 1rem; border: 1px solid var(--color-border);"
>
<h2 style="color: var(--color-text); font-size: 1.1rem; font-weight: 800; margin: 0 0 1rem;">
Дизайн-система через @theme
</h2>
<!-- Кольорова палітра -->
<p
style="color: var(--color-muted); font-size: 0.75rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em; margin: 0 0 0.5rem;"
>
Brand Palette
</p>
<div style="display: flex; gap: 0.25rem; margin-bottom: 1rem; border-radius: 0.5rem; overflow: hidden;">
<div style="height: 40px; flex: 1; background: var(--color-brand-50);"></div>
<div style="height: 40px; flex: 1; background: var(--color-brand-100);"></div>
<div style="height: 40px; flex: 1; background: var(--color-brand-200);"></div>
<div style="height: 40px; flex: 1; background: var(--color-brand-300);"></div>
<div style="height: 40px; flex: 1; background: var(--color-brand-400);"></div>
<div style="height: 40px; flex: 1; background: var(--color-brand-500);"></div>
<div style="height: 40px; flex: 1; background: var(--color-brand-600);"></div>
<div style="height: 40px; flex: 1; background: var(--color-brand-700);"></div>
<div style="height: 40px; flex: 1; background: var(--color-brand-800);"></div>
<div style="height: 40px; flex: 1; background: var(--color-brand-900);"></div>
</div>
<!-- Семантичні кольори -->
<p
style="color: var(--color-muted); font-size: 0.75rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em; margin: 0 0 0.5rem;"
>
Semantic Colors
</p>
<div style="display: flex; gap: 0.5rem; margin-bottom: 1rem;">
<div
style="padding: 0.4rem 0.75rem; background: var(--color-brand-500); color: white; border-radius: 100px; font-size: 0.75rem; font-weight: 700;"
>
Brand
</div>
<div
style="padding: 0.4rem 0.75rem; background: oklch(0.93 0.04 145); color: oklch(0.35 0.15 145); border-radius: 100px; font-size: 0.75rem; font-weight: 700;"
>
Success
</div>
<div
style="padding: 0.4rem 0.75rem; background: oklch(0.96 0.05 85); color: oklch(0.50 0.15 85); border-radius: 100px; font-size: 0.75rem; font-weight: 700;"
>
Warning
</div>
<div
style="padding: 0.4rem 0.75rem; background: oklch(0.95 0.04 25); color: oklch(0.45 0.18 25); border-radius: 100px; font-size: 0.75rem; font-weight: 700;"
>
Error
</div>
</div>
<!-- Компоненти що використовують токени -->
<p
style="color: var(--color-muted); font-size: 0.75rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em; margin: 0 0 0.5rem;"
>
Components using tokens
</p>
<div style="display: flex; flex-direction: column; gap: 0.5rem;">
<button
style="padding: 0.6rem 1.25rem; background: var(--color-brand-500); color: white; border: none; border-radius: 0.5rem; font-weight: 700; font-size: 0.875rem; cursor: pointer; align-self: flex-start;"
>
Primary Button — bg-brand-500
</button>
<div
style="display: flex; align-items: center; gap: 0.75rem; padding: 0.75rem 1rem; border: 1.5px solid var(--color-border); border-radius: 0.75rem; color: var(--color-text); font-size: 0.875rem;"
>
<span style="color: var(--color-muted);">Input через border-border →</span>
<input
type="text"
placeholder="border-border focus:border-brand-500"
style="border: 1.5px solid var(--color-border); border-radius: 0.5rem; padding: 0.35rem 0.65rem; font-size: 0.8rem; font-family: inherit; flex: 1; outline: none;"
/>
</div>
<p style="font-size: 0.875rem; color: var(--color-text); margin: 0; line-height: 1.6;">
Основний текст — <strong>text-text</strong>.<br />
<span style="color: var(--color-muted);"
>Приглушений текст — text-muted. Коли потрібно менший контраст.</span
>
</p>
</div>
</div>
@layer у взаємодії з @theme
Крім @theme, Tailwind v4 використовує @layer base, @layer components, @layer utilities:
@import 'tailwindcss';
@theme {
--color-brand-500: oklch(0.6 0.22 270);
--font-sans: 'Inter', system-ui, sans-serif;
}
/* Base: глобальні стилі без класів */
@layer base {
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
-webkit-text-size-adjust: 100%;
}
body {
font-family: var(--font-sans);
color: oklch(0.15 0.02 270);
line-height: 1.6;
}
h1,
h2,
h3,
h4,
h5,
h6 {
line-height: 1.2;
font-weight: 700;
}
a {
color: var(--color-brand-500);
text-decoration: underline;
}
img {
max-width: 100%;
display: block;
}
}
/* Components: компонентні класи */
@layer components {
.card {
@apply bg-white rounded-xl border border-slate-200 shadow-sm p-6;
}
.btn {
@apply inline-flex items-center justify-center gap-2 px-5 py-2.5
font-semibold rounded-xl transition-colors text-sm;
}
.btn-primary {
@apply bg-brand-500 text-white hover:bg-brand-600;
}
}
Порада: де зберігати теми
Для великих проєктів — розбийте CSS на логічні файли:
src/styles/
├── main.css ← @import "tailwindcss"; @import всього нижче
├── theme/
│ ├── colors.css ← @theme { --color-* }
│ ├── typography.css ← @theme { --font-*, --text-* }
│ └── tokens.css ← @theme { --spacing, --radius-*, --shadow-* }
├── base.css ← @layer base { ... }
└── components.css ← @layer components { @utility, .btn, .card }
/* main.css */
@import 'tailwindcss';
@import './theme/colors.css';
@import './theme/typography.css';
@import './theme/tokens.css';
@import './base.css';
@import './components.css';
Чисто, зрозуміло, масштабовано.
Завдання для самоперевірки
Завдання 1.1. Визначте в @theme дизайн-систему для медичного сайту:
- Основний колір: зелений (успіх, здоров'я) у OKLCH
- 5 відтінків (200, 400, 500, 600, 800)
- Другорядний: синій для акцентів
- Семантичні токени:
--color-bg,--color-text,--color-muted,--color-border - Шрифт: Inter для body + Merriweather для заголовків
Завдання 1.2. Поясніть різницю між:
/* Варіант A */
@theme {
--color-primary: oklch(0.6 0.2 270);
}
/* Варіант B */
@theme inline {
--color-primary: oklch(0.6 0.2 270);
}
Коли яку форму використовувати?
Завдання 1.3. Як скинути всі дефолтні кольори Tailwind і визначити тільки власні? Напишіть CSS із 5 власних кольорів.
Завдання 2.1. Design System для SaaS продукту.
Побудуйте повну тему:
@theme {
/* Кольори: 2 бренд-кольори × 9 відтінків */
/* Семантика: bg-base, bg-surface, bg-elevated, text-primary, etc. */
/* Шрифти: display для заголовків, sans для body */
/* Spacing: стандартна шкала */
/* Радіуси: sm/md/lg/xl/full */
/* Тіні: subtle/card/floating */
/* Анімації: 2+ кастомних */
}
Перевірте, що генеруються утиліти для всіх токенів.
Завдання 2.2. Компонентна бібліотека на вашій темі.
Створіть HTML-файл із такими компонентами (тільки через ваші кастомні класи):
- Кнопки: primary, secondary, ghost — всі три через
bg-brand-* - Картки з
shadow-cardрадіусомrounded-brand - Alert-компоненти для success/warning/error через ваші семантичні кольори
Завдання 2.3. Додайте підтримку dark mode через @theme:
- Визначте окремий набір пар
light: → dark:для семантичних токенів - Перемикання через
data-theme="dark"на<html> - Жодних
dark:класів у HTML — тільки CSS-змінні
Завдання 3.1. Multibrand тема.
Побудуйте систему для двох брендів, що використовують один код:
- Brand A: індиго + мінімалістичний дизайн
- Brand B: помаранчевий + більш "виразний" дизайн
/* Структура теми */
[data-brand='a'] {
/* токени для A */
}
[data-brand='b'] {
/* токени для B */
}
Один компонент кнопки — два бренди:
<div data-brand="a"><button class="btn btn-primary">A Brand Button</button></div>
<div data-brand="b"><button class="btn btn-primary">B Brand Button</button></div>
Обидві кнопки з btn btn-primary — але різний вигляд через зміну токенів.
Попередня стаття: Layout: Flexbox та GridНаступна стаття: Варіанти: hover, focus, responsive та нові v4
Layout: Flexbox та Grid через Tailwind
Майстерня Layout в Tailwind CSS: повний Flexbox та Grid через utility-класи. Типові layout-патерни: Holy Grail, Card Grid, Sidebar, Sticky Footer. Subgrid та практичні приклади.
Варіанти: hover, focus, responsive, dark mode та нові v4
Система варіантів Tailwind CSS v4: стани взаємодії, structural pseudo-classes, group та peer, нові варіанти has-*, not-*, nth-*, starting:, адаптивні breakpoints та dark mode.