Постановка задачи
Нужно создать цветовой модуль дизайн-системы, совместимый с Tailwind CSS. Кроме того, дизайн-система потенциально должна поддерживать смену тем (не только на тёмную, но и на другие, которые могут не быть парными “тёмная + светлая”).
Палитра цветов
Начальная палитра для этого примера такая, в реальности их больше.
@theme {
--color-primary: #42a66a;
--color-primary-active: #2e7349;
--color-primary-bg: #edfaf2;
--color-base-800: #333840;
--color-base-600: #58606e;
--color-base-400: #8e99ab;
--color-base-borders: #c8d1e0;
--color-base-bg: #f5f7fa;
--color-system-negative: #de1b1b;
--color-system-positive: #38bda1;
}
Уровни текста
Изначально хотелось использовать названия text-primary, text-secondary и text-tertiary в соответствии с их иерархией на странице. Но из-за того, что Tailwind резервирует такие названия под брендовые цвета, то получается, что text-primary будет означать “текст цвета primary color”, а не “главный текст”.
В качестве альтернативы можно было бы использовать text-high, text-medium, text-low. Но так как это может потенциально пересекаться с классом font-medium, то пришлось отказаться и от этого варианта.
Везде по умолчанию цвет считается главным (базовым), если мы не применяем специальные классы вроде text-secondary.
Иерархия текста:
Вместо цифровых индексов (text-base-800, text-base-600) мы вводим семантические роли, вдохновленные GitHub Primer и shadcn/ui:
| Роль | Название | Применение |
|---|---|---|
| Основной | text-main | Заголовки, основной текст |
| Второстепенный | text-muted | Даты, мета-данные, подписи |
| Третичный | text-subtle | Плейсхолдеры, дисклеймеры |
Но на данный момент у нас получается тавтология:
<div class="text-text-secondary bg-primary-bg border-base-borders"></div>
Здесь у нас есть развилка: можно использовать абсолютные или относительные цвета.
1. Абсолютные цвета
Назначаем цвета muted (secondary) и subtle (tertiary) вручную.
Theme:
@theme {
--color-text-main: var(--color-base-800);
--color-text-muted: var(--color-base-600);
--color-text-subtle: var(--color-base-400);
}
Utilities:
@utility text-main {
color: var(--text-main-color);
}
@utility text-muted {
color: var(--text-muted-color);
}
@utility text-subtle {
color: var(--text-subtle-color);
}
2. Относительные цвета
Цвета muted и subtle назначаются через прозрачность. Для этого дополнительно нужно создать утилити-классы для текста.
@utility text-main {
color: var(--text-color);
}
@utility text-muted {
color: color-mix(in srgb, var(--text-color), transparent 30%);
}
@utility text-subtle {
color: color-mix(in srgb, var(--text-color), transparent 60%);
}
К muted добавляется 30% прозрачности, соответственно как opacity 0.7, а к subtle 60% (до opacity 0.4).
Тогда в теме вместо base-800, base-600, base-400, указываем:
@theme {
--color-text-main: var(--color-slate-800);
--color-text-inverted: var(--color-white);
}
Концепция Surface (Поверхности)
Идея состоит в том, чтобы использовать один класс вместо двух. То есть, в отличие от Material Design 3 (M3) и других дизайн-систем, вместо связки primary-background + primary-foreground или primary + on-primary, используется один класс surface-primary, который объединяет в себе фон + цвет текста на нём.
Поверхность сама знает, какой текст на ней должен находиться. Ранее мы установили правило иерархии текста на 3 типа. Теперь поверхность будет устанавливать свой фон и перезаписывать переменные цвета текста. Благодаря наследованию color, такая концепция позволяет иметь вложенные элементы surface.
Необходимо сделать допущение, что цвет фона мы используем без прозрачности. Иначе комбинации одного фона над другим могут быть непредсказуемыми.
Для body не забываем установить любой из созданных нами surface.
Далее под наши 2 варианта реализации:
1. Абсолютные цвета
Utilities:
@utility surface-base {
--text-main-color: var(--color-base-800);
--text-muted-color: var(--color-base-600);
--text-subtle-color: var(--color-base-400);
color: var(--text-main-color);
}
@utility surface-base-inverted {
--text-main-color: var(--color-white);
--text-muted-color: rgba(255, 255, 255, 0.7);
--text-subtle-color: rgba(255, 255, 255, 0.4);
color: var(--text-main-color);
}
@utility surface-default {
@apply surface-base bg-white;
}
@utility surface-muted {
@apply surface-base bg-base-bg;
}
@utility surface-primary-muted {
@apply surface-base bg-primary-bg;
}
@utility surface-primary {
@apply surface-base-inverted bg-primary;
}
@utility surface-primary-hover {
@apply surface-base-inverted bg-primary-active;
}
surface-base и surface-base-inverted - это mixin-утилита. Хотя в инвертированных поверхностях не используются text-muted и text-subtle, но на всякий случай, им всё равно прописаны цвета через прозрачность.
2. Относительные цвета
Отличия только в миксине:
@utility surface-base {
--text-color: var(--color-text-main);
color: var(--text-color);
}
@utility surface-base-inverted {
--text-color: var(--color-text-inverted);
color: var(--text-color);
}
Варианты наименований
Числовой:
surface-100илиsurface-0– базовый фон (Canvas/Body).surface-200илиsurface-1– первый уровень вложенности (карточки, сайдбары).surface-300илиsurface-2– второй уровень (инпуты, вложенные списки).surface-400илиsurface-3– акцентные или всплывающие элементы.
Семантический:
surface-default(илиbase) – основной фон.surface-muted– приглушенный фон для второстепенных панелей.surface-card– поверхность для контентных блоков.surface-overlay– для модалок и меню.surface-accent– для элементов, которые должны бросаться в глаза.
Material Design:
surface-container-lowest– самый дальнийsurface-container-low– подложкаsurface-container– стандартныйsurface-container-high– выделенныйsurface-container-highest– самый ближний
Кроме того, должны использоваться ситуативные поверхности:
surface-infosurface-successsurface-errorsurface-warningsurface-neutral
Для градаций “хорошо-плохо”:
surface-positivesurface-normalsurface-negative
Отдельно отмечу варианты для небольшого выделения цветом:
surface-primary-softилиsurface-primary-subtleдля поверхностей сbg-primary-50surface-primary-mutedдля поверхностей сbg-primary-100surface-base-softилиsurface-neutral-softили простоsurface-soft
Для отдельных поверхностей могут использоваться свои ситуативные токены, но лучше не злоупотреблять ими:
surface-headersurface-footer
Пример использования в компоненте
<div class="surface-default border-base-borders border p-4">
<h2 class="text-xl font-semibold">Пример</h2>
<p class="text-muted mt-2">Второстепенное описание</p>
<div class="surface-muted mt-4 rounded p-2">Акцентный блок внутри</div>
</div>
Пример
Второстепенное описание
Выводы
Плюсы surface:
- Цвет текста не надо переназначать. мы задаём тексту роли, которые автоматом перекрашиваются на своей поверхности, в том числе при смене темы.
- Всем базовым текстам не надо задавать классы вроде
text-foreground. Особые классы даются только второстепенному и третичному цвету.
Минусы surface:
- Повторение цветовых стилей (
--text-secondary-color) для каждой поверхности.
Некоторые surface вторичные, для них не предполагается secondary, tertiary текстов, поэтому для них он может не указываться.