Темы и дизайн-токены
@theme, семантические токены, светлая/тёмная тема на CSS-переменных и ручной переключатель.
Обновлено
Что такое тема
Тема — набор визуальных решений продукта: цвета, шрифты, скругления, тени, отступы. В хорошо построенном проекте эти решения собраны в одном месте в виде дизайн-токенов — именованных переменных, — а компоненты используют только токены.
Итог: меняете бренд, добавляете тёмную тему или второй бренд в одном файле, не трогая компоненты.
Токены, а не захардкоженные цвета
Классическая ошибка: bg-blue-600, разбросанный по 200 файлам. Сменили бренд → правите 200 файлов.
Правильно: определить семантические дизайн-токены (какую роль играет цвет, а не какой это оттенок) и использовать их везде.
примитивные: blue-600, stone-100 → сырая палитра
семантические: accent, bg, surface, muted → то, что использует UI
@theme — токены, которые становятся классами
@import "tailwindcss";
@theme {
--color-accent: #1f5fbf;
--font-display: "IBM Plex Sans", sans-serif;
--radius-card: 0.75rem;
}Каждая переменная в нужном пространстве имён генерирует классы: --color-accent → bg-accent, text-accent, border-accent/50; --font-display → font-display; --radius-card → rounded-card.
| Пространство имён | Классы |
|---|---|
--color-* |
bg-*, text-*, border-*, fill-*… |
--font-* |
font-* |
--text-* |
text-* (размеры) |
--spacing |
база для p-*, m-*, gap-* |
--radius-* |
rounded-* |
--shadow-* |
shadow-* |
--breakpoint-* |
sm:, md:… |
Светлая / тёмная на CSS-переменных
Паттерн, который использует само это приложение:
:root {
--bg: #f7f6f3;
--fg: #1f1e1c;
--accent: #1f5fbf;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #141414;
--fg: #e9e7e2;
--accent: #6ea4f0;
}
}
@theme inline {
--color-bg: var(--bg);
--color-fg: var(--fg);
--color-accent: var(--accent);
}@theme inline = класс использует var(--bg) напрямую, поэтому меняется вместе с переменной. Компоненты пишут просто bg-bg text-fg — dark: им вообще не нужен.
Ручной переключатель (кнопка темы)
По умолчанию dark: следует системе. Для кнопки, которая принудительно включает тему:
@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));
[data-theme=dark] {
--bg: #141414;
--fg: #e9e7e2;
}Ставите data-theme="dark" на <html> (сохраняется в cookie/localStorage, применяется до гидратации, чтобы не мигало).
Светлая / тёмная / системная без «вспышки»
У настоящего переключателя три варианта: светлая, тёмная и «как в системе». Классическая проблема: страница рендерится на сервере, не зная выбора пользователя → на долю секунды показывается не та тема (вспышка), потом JS её исправляет.
| Где хранить выбор | Как избежать вспышки |
|---|---|
| cookie | сервер читает cookie и сразу ставит data-theme на <html> — вспышки нет (страница становится динамической) |
localStorage |
маленький скрипт прямо в <head> ставит data-theme до отрисовки страницы — так делает библиотека next-themes |
/* «системная» = без атрибута → следует prefers-color-scheme */
@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));
@media (prefers-color-scheme: dark) {
:root:not([data-theme=light]) { --bg: #141414; --fg: #e9e7e2; }
}
[data-theme=dark] { --bg: #141414; --fg: #e9e7e2; }Добавьте ещё color-scheme: light dark на :root — полосы прокрутки и нативные элементы (input, select) подстроятся автоматически.
Несколько тем / брендов
Тот же механизм: [data-theme=ocean] { --accent: ... }. Компоненты не меняются вообще.
Правила
- Компоненты используют только семантические токены (
bg-surface,text-muted), а неbg-stone-100. - Немного токенов с хорошими именами:
bg,surface,fg,muted,line,accent,success,danger. - Проверяйте контраст в обеих темах.
Коротко
- Семантические токены (
bg,surface,accent), а не сырые цвета в компонентах. @themeсоздаёт классы;@theme inline+ CSS-переменные = темы, меняющиеся во время работы.- Тёмная тема и несколько брендов = другие значения тех же переменных.