37 Модуль 8 Архитектура стилей 17 мин урок 12 из 25

Дизайн-токены и темизация

Три уровня переменных, из-за которых третья тема добавляется без единой правки в компонентах. И почему --color-danger лучше, чем --red-500.

Что ты научишься делать

  • Разложишь переменные на три уровня и поймёшь, зачем это нужно
  • Добавишь третью тему, не тронув ни одного компонента
  • Уберёшь мигание темы при загрузке страницы

#Проблема появляется на второй теме

С одной темой переменные кажутся излишеством: ну да, --purple: #6544e0 вместо цвета в двадцати местах. Удобно, но не революция.

Разница видна, когда просят тёмную тему. И становится болезненной, когда после неё просят контрастную — для слабовидящих.

Если компоненты ссылаются на --purple, в тёмной теме придётся переопределить --purple на не-фиолетовый. Имя начинает врать. Дальше кто-то заводит --purple-dark, и через месяц никто не понимает, какую переменную брать.

#Три уровня

Решение — разделить переменные по слоям. Каждый следующий ссылается на предыдущий.

УровеньЧто содержитПример
Сырые значенияПалитра. Названия про цвет, не про смысл--violet-600: #6544e0
СемантическиеРоль в интерфейсе. Меняются от темы к теме--color-accent: var(--violet-600)
КомпонентныеТочка настройки конкретного блока--btn-bg: var(--color-accent)
CSStokens.css
/* Уровень 1: палитра. Одна на весь проект, темы её не трогают */:root {  --violet-600: #6544e0;  --violet-300: #b9a5ff;  --red-600: #c8394a;  --gray-900: #16181d;  --gray-100: #f4f4f8;  --white: #fff;}/* Уровень 2: смысл. Вот это и переключают темы */:root,[data-theme="light"] {  --color-surface: var(--white);  --color-text: var(--gray-900);  --color-accent: var(--violet-600);  --color-danger: var(--red-600);}[data-theme="dark"] {  --color-surface: var(--gray-900);  --color-text: var(--gray-100);  --color-accent: var(--violet-300);  --color-danger: #f2777a;}/* Уровень 3: компонент. Нужен не всегда, только для точек настройки */.btn {  --btn-bg: var(--color-accent);  background: var(--btn-bg);}.btn--danger {  /* Меняем одну переменную вместо переписывания правил */  --btn-bg: var(--color-danger);}

Интерактивный разбор доступен при включённом JavaScript.

#Компонентные токены — точки настройки

Третий уровень нужен не везде. Он окупается там, где у компонента есть варианты: вместо переписывания правил меняется одна переменная.

Так не надо

.btn--danger {
  background: var(--color-danger);
  border-color: var(--color-danger);
}

.btn--danger:hover {
  background: var(--color-danger-dark);
}

Каждый вариант переписывает все правила заново. Появится третий — придётся копировать снова.

Так надо

.btn {
  --btn-bg: var(--color-accent);
  background: var(--btn-bg);
  border-color: var(--btn-bg);
}

.btn--danger { --btn-bg: var(--color-danger); }
.btn--ok { --btn-bg: var(--color-ok); }

Вариант — это одна строка. Логика оформления описана один раз, в блоке.

#Переключение без мигания

Классическая беда: страница на долю секунды показывается светлой, потом становится тёмной. Причина в том, что тему применяет скрипт, который выполняется после отрисовки.

Лечится встроенным скриптом в head — до того, как браузер что-либо нарисует. Он маленький и намеренно не вынесен в файл: внешний файл пришлось бы ждать.

HTMLПервым делом в head
<script>  // Выполняется до первой отрисовки — поэтому мигания нет  (function () {    try {      var saved = localStorage.getItem('theme');      var system = matchMedia('(prefers-color-scheme: dark)').matches        ? 'dark' : 'light';      document.documentElement.dataset.theme = saved || system;    } catch (err) {      document.documentElement.dataset.theme = 'light';    }  })();</script>

#color-scheme и light-dark()

Свойство color-scheme сообщает браузеру, какие темы поддерживает страница. От него зависят системные элементы: полосы прокрутки, поля ввода, выпадающие списки. Без него в тёмной теме останутся белые скроллбары.

Функция light-dark() — недавнее сокращение: два значения в одной строке вместо двух блоков переопределения.

CSSКороткая запись
:root {  /* Обязательно: иначе системные элементы останутся светлыми */  color-scheme: light dark;  /* Первое значение для светлой темы, второе для тёмной */  --color-surface: light-dark(#fff, #16181d);  --color-text: light-dark(#16181d, #e8eaf2);}

#Контраст в тёмной теме

Тёмная тема — это не инверсия светлой. Если просто поменять чёрное на белое, получится неприятный результат: чистый белый текст на чистом чёрном фоне даёт слишком высокий контраст, от него быстро устают глаза.

Работающий приём — смягчить оба конца: фон не абсолютно чёрный, текст не абсолютно белый.

НаивноКак делают
Фон#000000#0b0e17 — почти чёрный с оттенком
Основной текст#ffffff#e8eaf2 — сбавленная яркость
Второстепенный текст#888888#a7adc2 — светлее, чем кажется нужным
АкцентТот же, что в светлойСветлее: насыщенные цвета на тёмном темнеют визуально

#Не только цвета

Токенами описывают всё, что должно быть согласованным: отступы, радиусы, тени, скорости анимации, размеры шрифта.

Польза та же, что и с цветом: значения перестают быть случайными. Отступ 13px в одном месте и 14px в другом — это не решение дизайнера, это опечатка, которую шкала не пропустит.

CSSШкалы
:root {  /* Шаг шкалы, а не произвольные числа */  --space-1: 4px;  --space-2: 8px;  --space-3: 12px;  --space-4: 16px;  --space-6: 24px;  --radius-sm: 8px;  --radius-md: 14px;  --radius-full: 999px;  --speed-fast: .15s;  --speed-base: .3s;  --ease: cubic-bezier(.22, 1, .36, 1);}

#Как это выглядит в реальном проекте

CSStheme.css
@layer tokens {  :root {    color-scheme: light dark;    /* Палитра: не меняется никогда */    --violet-600: #6544e0;    --violet-300: #b9a5ff;    --gray-950: #0b0e17;    --gray-050: #f6f7fb;  }  /* Светлая — она же значение по умолчанию */  :root,  [data-theme="light"] {    --color-bg: var(--gray-050);    --color-text: #131627;    --color-accent: var(--violet-600);    --color-border: rgb(16 20 40 / .1);  }  [data-theme="dark"] {    --color-bg: var(--gray-950);    --color-text: #e8eaf2;    --color-accent: var(--violet-300);    --color-border: rgb(255 255 255 / .08);  }  /* Третья тема — те же имена, другие значения.     Ни один компонент об этом не знает */  [data-theme="contrast"] {    --color-bg: #000;    --color-text: #fff;    --color-accent: #ffe600;    --color-border: #fff;  }}

#Когда НЕ надо

  • Три уровня на лендинге. Одна тема, десять цветов — хватит одного уровня семантических переменных. Палитра отдельно тут только добавит файлов.
  • Токен на каждое значение. Если переменная используется один раз в одном месте, она не токен, а лишний уровень косвенности.
  • Переменные внутри медиазапросов ради адаптива. Работает, но отлаживать тяжело: значение меняется невидимо. Для размеров лучше clamp().
  • Хранить в токенах логику. Переменная — это значение. Если хочется положить туда целое правило, нужен класс, а не токен.

#Поддержка браузерами

ВозможностьСостояние
Пользовательские свойства --xВезде, много лет
color-schemeВо всех современных браузерах
prefers-color-schemeВо всех современных браузерах
light-dark()Появилась недавно; где не поддержана, значение игнорируется — нужен запасной
prefers-contrastПоддерживается, но по-разному; лучше давать явный переключатель

Где спотыкаются почти все

Компоненты ссылаются на палитру напрямую

Почему В тёмной теме приходится переопределять --violet-600 на не-фиолетовый. Имя начинает врать, и появляются переменные-двойники.

Как надо Компонент знает только семантические имена. Палитру трогает лишь второй уровень.

Тема мигает при загрузке

Почему Скрипт применяет тему после отрисовки, и человек успевает увидеть светлую вспышку.

Как надо Встроенный скрипт в head, до стилей и до разметки.

Забыт color-scheme

Почему Полосы прокрутки, поля ввода и выпадающие списки остаются светлыми в тёмной теме.

Как надо Пиши color-scheme: light dark в :root.

Тема переключается классом на body

Почему Стили из head применяются раньше, чем разметка body, — снова мигание. Плюс до body не дотянуться из :root.

Как надо Ставь атрибут на documentElement, то есть на <html>.

Дизайн-токен design token
Именованное значение оформления: цвет, отступ, радиус, скорость.
Сырое значение primitive token
Палитра. Название описывает само значение: --violet-600.
Семантический токен semantic token
Название описывает роль: --color-danger. Именно они переключаются темами.
Компонентный токен component token
Точка настройки конкретного блока: --btn-bg.
Цветовая схема color-scheme
Свойство, сообщающее браузеру о поддерживаемых темах. Влияет на системные элементы.

Проверь себя

  1. Почему компонент должен ссылаться на <code>--color-danger</code>, а не на <code>--red-600</code>?

    Роль сохраняется при любой теме, конкретный цвет — нет.

  2. Как убрать мигание темы при загрузке?

    Тема должна быть установлена раньше, чем браузер что-либо нарисует. Внешний файл для этого слишком медленный.

  3. Что сломается без <code>color-scheme: light dark</code>?

    Своими стилями системные элементы не покрасить. Об этом говорит именно <code>color-scheme</code>.

  4. Зачем нужен третий, компонентный уровень токенов?

    Логика оформления описывается один раз в блоке, а модификатор меняет только значение.

  5. Почему атрибут темы ставят на <code>&lt;html&gt;</code>, а не на <code>&lt;body&gt;</code>?

    Корневой элемент существует уже во время выполнения скрипта в <code>head</code>, а <code>body</code> ещё нет.

Задание

Третья тема без правки компонентов

В стилях есть светлая и тёмная темы, но компоненты ссылаются прямо на палитру — поэтому тёмная работает наполовину.

Наведи порядок: заведи семантические токены (--color-surface, --color-text, --color-accent), переведи компоненты на них, а потом добавь третью тему [data-theme="contrast"], не трогая ни одного правила компонента. Не забудь color-scheme.

Открыть в песочнице
    Подсказка 1

    Сначала заведи семантический слой: :root { --color-surface: var(--white); --color-text: var(--gray-900); --color-accent: var(--violet-600); }

    Подсказка 2

    Потом замени в компонентах: background: var(--color-surface); color: var(--color-text); и так далее.

    Подсказка 3

    Третья тема — те же имена, другие значения: [data-theme="contrast"] { --color-surface: #000; --color-text: #fff; --color-accent: #ffe600; }

    Показать решение
    HTMLindex.html
    <div data-theme="contrast" class="page">  <article class="card">    <h3 class="card__title">Раф на кокосовом</h3>    <p class="card__text">Мягкий вкус</p>    <button class="btn">В корзину</button>  </article></div>
    CSSstyle.css
    :root {  color-scheme: light dark;  /* Палитра */  --violet-600: #6544e0;  --gray-900: #16181d;  --gray-100: #f4f4f8;  --white: #fff;  /* Смысл: только эти имена знают компоненты */  --color-bg: var(--gray-100);  --color-surface: var(--white);  --color-text: var(--gray-900);  --color-accent: var(--violet-600);  --color-on-accent: var(--white);}[data-theme="contrast"] {  --color-bg: #000;  --color-surface: #000;  --color-text: #fff;  --color-accent: #ffe600;  --color-on-accent: #000;}.page { padding: 20px; background: var(--color-bg); }.card {  padding: 16px;  background: var(--color-surface);  color: var(--color-text);  border-radius: 14px;}.card__title { margin: 0 0 6px; font-size: 17px; }.card__text { margin: 0 0 12px; font-size: 14px; }.btn {  padding: 9px 16px;  font: inherit;  color: var(--color-on-accent);  background: var(--color-accent);  border: 0;  border-radius: 999px;}

    Проверь себя