Перейти к содержимому
Плотность
Тема

Основания

Цвет

Два яруса. Внизу рампы — сырые шаги, они не меняются никогда. Сверху семантика — для чего цвет нужен. Компонент видит только семантику: тот, кто написал --n-3, только что захардкодил светлую тему.

Компонент
Прогон #4127 идёт
Три теста упали после последнего прохода.
Разметка
<div class="inst-panel">
  <div class="inst-panel-header">
    <span class="inst-panel-title">Прогон #4127</span>
    <span class="inst-badge" data-tone="running"><span class="inst-dot"></span>идёт</span>
  </div>
  <div class="inst-panel-body inst-stack">
    <input class="inst-input" type="text" value="terrain_chunk_04">
    <div class="inst-note" data-tone="warn">Три теста упали после последнего прохода.</div>
  </div>
</div>

Использование

Компонент обращается к семантике, и только к ней. Рампа — сырьё, из которого семантика сделана, и в разметке ей делать нечего.

.my-thing {
  color: var(--text-primary);
  background: var(--surface-raised);
  border: var(--hairline) solid var(--border);
}
Что Обязательно Почему
Только ярус семантики да Тот, кто написал --n-3, только что захардкодил светлую тему: рампа между темами не меняется
Тон через data-tone, а не свой цвет да Пять значений, один словарь на весь кит. Шестое не покрасится
Два передних плана по работе да --tone-ink для текста (4.5:1), --tone-mark для метки без подписи (3:1)
Цвет не единственный носитель да Состояние ходит со словом и знаком. Первый закон кита

Тема на части экрана

Тема объявлена атрибутом и работает на любом поддереве. inst-theme добавляет к атрибуту то, чего одному атрибуту не хватает: color наследуется вычисленным значением, и узел внутри тёмной области без неё унаследовал бы чёрный текст от светлого предка.

Пример
Тёмная область
Светлая страница, тёмная панель. Один атрибут и один класс.
Разметка
<div class="inst-theme" data-theme="dark" style="padding: var(--pad-panel); border-radius: var(--radius-lg)">
  <div class="inst-panel">
    <div class="inst-panel-header"><span class="inst-panel-title">Тёмная область</span></div>
    <div class="inst-panel-body">Светлая страница, тёмная панель. Один атрибут и один класс.</div>
  </div>
</div>

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

Когда использовать

Используйте Возьмите другое
Семантический токен: --text-primary, --surface-raised, --ok-text Шаг рампы напрямую (--n-3, --a-4) — это светлая тема, вписанная в компонент. Список семантики целиком — токены
Состояние объекта — атрибут data-tone, один словарь на весь кит Свой цвет под своё состояние — заведите тон и покажите его бейджем: цвет не имеет права быть единственным носителем
Ряд графика — --chart-1--chart-6 по порядку Статусный тон как «ещё одна серия» — тона ok/warn/error зарезервированы. Ряды подписывает легенда
Глубина — перепад поверхностей Тень ради глубины — тень означает «временное и сверху», см. высоту
Информационное сообщение — тон neutral Синий как «info» — синий занят акцентом и состоянием «идёт», см. сноску в карточке

Устройство

Нейтраль — 15 шагов

Пример
01234 56789 1011121314
Разметка
<div class="ramp">
  <span class="ramp-step" style="--c: var(--n-0)"></span>
  <span class="ramp-step" style="--c: var(--n-1)"></span>
  <span class="ramp-step" style="--c: var(--n-2)"></span>
  <span class="ramp-step" style="--c: var(--n-3)"></span>
  <span class="ramp-step" style="--c: var(--n-4)"></span>
  <span class="ramp-step" style="--c: var(--n-5)"></span>
  <span class="ramp-step" style="--c: var(--n-6)"></span>
  <span class="ramp-step" style="--c: var(--n-7)"></span>
  <span class="ramp-step" style="--c: var(--n-8)"></span>
  <span class="ramp-step" style="--c: var(--n-9)"></span>
  <span class="ramp-step" style="--c: var(--n-10)"></span>
  <span class="ramp-step" style="--c: var(--n-11)"></span>
  <span class="ramp-step" style="--c: var(--n-12)"></span>
  <span class="ramp-step" style="--c: var(--n-13)"></span>
  <span class="ramp-step" style="--c: var(--n-14)"></span>
</div>
<div class="ramp ramp-scale">
  <span>0</span><span>1</span><span>2</span><span>3</span><span>4</span>
  <span>5</span><span>6</span><span>7</span><span>8</span><span>9</span>
  <span>10</span><span>11</span><span>12</span><span>13</span><span>14</span>
</div>

Переключите тему стола: рампа не изменится. Она сырая и не знает о темах — меняется только то, какие её шаги берёт семантика.

Одна ручка задаёт направление уклона: --hue-neutral: 75 — тёплый, 250 — холодный. Цветность 0.002–0.006: ниже порога осознанного замечания, и в этом её работа. Пятнадцатый шаг существует потому, что тёмному концу нужно четыре различимые поверхности подряд, а на четырнадцати их помещалось три.

Шаг Светлота Где занят
--n-0 0.994 --surface-raised, --surface-overlay (светлая), --accent-on
--n-1 0.978 --surface-page, --surface-field (светлая), --text-primary (тёмная)
--n-2 0.958 --surface-sunken (светлая)
--n-3 0.928 Резерв
--n-4 0.884 Резерв
--n-5 0.806 --text-secondary (тёмная)
--n-6 0.706 --text-muted (тёмная)
--n-7 0.606 --text-faint (обе темы)
--n-8 0.508 --text-muted (светлая)
--n-9 0.416 --text-secondary (светлая)
--n-10 0.322 --surface-overlay (dark-soft)
--n-11 0.242 --surface-overlay (тёмная), --surface-raised (dark-soft)
--n-12 0.196 --text-primary (светлая), --surface-raised (тёмная), --surface-page (dark-soft)
--n-13 0.155 --surface-page (тёмная), --surface-sunken и --surface-field (dark-soft)
--n-14 0.120 --surface-sunken, --surface-field (тёмная)

Шаги без семантики — резерв под приложение и графики.

Акцент — один тон

Пример
123456
Разметка
<div class="ramp">
  <span class="ramp-step" style="--c: var(--a-1)"></span>
  <span class="ramp-step" style="--c: var(--a-2)"></span>
  <span class="ramp-step" style="--c: var(--a-3)"></span>
  <span class="ramp-step" style="--c: var(--a-4)"></span>
  <span class="ramp-step" style="--c: var(--a-5)"></span>
  <span class="ramp-step" style="--c: var(--a-6)"></span>
</div>
<div class="ramp ramp-scale">
  <span>1</span><span>2</span><span>3</span><span>4</span><span>5</span><span>6</span>
</div>

Второй акцентный тон не заводится. Это не запрос фичи, а сигнал, что смысл должно нести что-то другое.

Шаг Светлота Где занят
--a-1 0.960 --accent-bg (светлая)
--a-2 0.890 Резерв
--a-3 0.700 --accent-text, --accent-mark, --accent-border, --focus-ring (тёмная)
--a-4 0.560 --accent-solid, --accent-mark, --accent-border, --focus-ring (светлая)
--a-5 0.450 --accent-text (светлая), --accent-hover
--a-6 0.330 Резерв

Тон — 250°. Светлота --accent-solid ограничена сверху: светлее 0.56 белая подпись перестаёт держать 4.5:1.

Статусы — три зарезервированных тона

Пример
13456
Разметка
<div class="ramp">
  <span class="ramp-step" style="--c: var(--ok-1)"></span>
  <span class="ramp-step" style="--c: var(--ok-2)"></span>
  <span class="ramp-step" style="--c: var(--ok-3)"></span>
  <span class="ramp-step" style="--c: var(--ok-4)"></span>
  <span class="ramp-step" style="--c: var(--ok-5)"></span>
  <span class="ramp-step" style="--c: var(--ok-6)"></span>
</div>
<div class="ramp">
  <span class="ramp-step" style="--c: var(--warn-1)"></span>
  <span class="ramp-step" style="--c: var(--warn-3)"></span>
  <span class="ramp-step" style="--c: var(--warn-4)"></span>
  <span class="ramp-step" style="--c: var(--warn-5)"></span>
  <span class="ramp-step" style="--c: var(--warn-6)"></span>
</div>
<div class="ramp">
  <span class="ramp-step" style="--c: var(--err-1)"></span>
  <span class="ramp-step" style="--c: var(--err-3)"></span>
  <span class="ramp-step" style="--c: var(--err-4)"></span>
  <span class="ramp-step" style="--c: var(--err-5)"></span>
  <span class="ramp-step" style="--c: var(--err-6)"></span>
</div>
<div class="ramp ramp-scale">
  <span>1</span><span>3</span><span>4</span><span>5</span><span>6</span>
</div>

Шага 2 в статусных рядах нет — он не понадобился ни одной роли.

ok — 150°, warn — 85°, err — 25°. Никогда не используются как украшение или как «четвёртая серия на графике».

Шаг Роль Почему именно так
1 Тонированный фон в светлой теме --ok-1 --warn-1 --err-1
2 Текст на приподнятых тёмных поверхностях Существует ради dark-light: она ставит панель на --n-10, а там шаг 3 даёт 3.70
3 Текст в тёмных темах на дне рампы Светлее соседей по ряду
4 Заливка и индикатор в светлой --ok-4 --warn-4 --err-4
5 Текст в светлой, поверх шага 1 Шаг 4 не вытягивает 4.5:1 на собственном тонированном фоне
6 Резерв под графики

Шаг 5 существует именно потому, что шаг 4 не проходит порог на самом себе. Пока его не было, жёлтый бейдж жил на контрасте 3.02.

Шкала

Одно объявление на токен через light-dark(). Второй темы как отдельного блока не существует — значит, и расходиться нечему.

Поверхности

Пример
--surface-sunken
--surface-page
--surface-raised
--surface-overlay
--surface-field
Разметка
<div class="swatches">
  <div class="sw"><div class="sw-chip" style="--c: var(--surface-sunken)"></div><div class="sw-cap">--surface-sunken</div></div>
  <div class="sw"><div class="sw-chip" style="--c: var(--surface-page)"></div><div class="sw-cap">--surface-page</div></div>
  <div class="sw"><div class="sw-chip" style="--c: var(--surface-raised)"></div><div class="sw-cap">--surface-raised</div></div>
  <div class="sw"><div class="sw-chip" style="--c: var(--surface-overlay)"></div><div class="sw-cap">--surface-overlay</div></div>
  <div class="sw"><div class="sw-chip" style="--c: var(--surface-field)"></div><div class="sw-cap">--surface-field</div></div>
</div>

Переключите тему стола — вот здесь всё и меняется. Рампа осталась прежней, а семантика взяла другие её шаги: в тёмной теме перепад между соседними поверхностями шире, потому что тёмная тема не инверсия, а авторская.

Читаются как стопка. Глубина передаётся порядком светлоты и никогда тенью.

Токен Работа
--surface-sunken Дно: врез, дорожка, шапка таблицы
--surface-page Фон документа
--surface-raised Панель, карточка, шапка оболочки
--surface-overlay Поповер, меню, тултип, модалка
--surface-field Врез под ввод. Отдельная роль, а не переиспользование raised: пока поле красилось цветом панели, его единственной границей была рамка на 1.31:1
--surface-hover --surface-active Альфа поверх чего угодно
--surface-selected Выделение. Тоже альфа — чтобы наведение по выделенной строке оставалось видно

Текст

Токен Порог Для чего
--text-primary 4.5:1 Данные, заголовки, подписи контролов
--text-secondary 4.5:1 Второстепенное: описания, единицы
--text-muted 4.5:1 Метаданные и таймстемпы — это данные, им положен читаемый порог
--text-faint 3:1 Не применяется к тексту, который читают. Порог декорации

Рамки

Несущая и декоративная — разные токены, потому что к ним разные требования.

Токен Когда
--border-subtle Разделитель внутри уже ограниченной области
--border Панель, карточка, ячейка: рядом есть перепад поверхностей
--border-strong Скроллбар, акцентированный шов
--border-control Граница, которая и есть контрол: чекбокс, поле, дорожка свитча. Обязана держать 3:1

Тоны

У смыслового тона три токена: текст, метка и фон.

Текст и метка разошлись по той же причине, что у акцента. Шаг 5 рассчитан на чтение, и пока тон стоял в подписи бейджа, разницы не было. На заливке во всю строку — полоса истории, дуга кольца, лента меры — он читается темнее и глуше, чем акцентная метка рядом, и палитра распадается на два регистра. Метке довольно 3:1, и на шаге 4 все метки кита стоят в одном.

Тон Текст (4.5:1) Метка (3:1) Фон
акцент --accent-text --accent-mark --accent-bg
ok --ok-text --ok-mark --ok-bg
warn --warn-text --warn-mark --warn-bg
error --err-text --err-mark --err-bg

Третьего — «заливки» между текстом и фоном — нет. Он жил на шаге 4 и не проходил там, где под ним оказывалась дорожка: жёлтая заливка меры давала 2.49 при норме 3.0.

У акцента есть случай, которого нет у статусов, — заливка под белой подписью (--accent-solid и --accent-on).

Тон как атрибут

data-tone ставится один раз на группу; вложенные элементы читают --tone-ink, --tone-mark и --tone-bg и больше ничего о тоне не знают.

Значение Значит
neutral Явно нейтральное. Оно же исполняет роль info
running Идёт сейчас. Носитель — пульсация, тон вторичен
ok Успешно завершено
warn Завершено с замечаниями
error Упало

Словарь закрыт. Шестое значение не заводится.

Варианты

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

Светлые различаются уклоном, тёмные — глубиной, и это не небрежность. На светлом фоне глаз ловит температуру: тёплая бумага и холодная бумага читаются как разные. На тёмном температуру почти не видно, зато сразу заметно, насколько панель отделяется от страницы. Ручки разные, потому что в светлом и тёмном замечают разное.

Атрибут color-scheme --hue-neutral --tint Что ещё
нет по системе 75 1 Слушает настройку ОС
data-theme="light-neutral" light 0 Уклон выключен: чистый серый
data-theme="light" light 75 1 Тёплая
data-theme="light-cool" light 250 1 Холодная
data-theme="dark-light" dark 75 1 Стопка поднимается на две ступени. Потолок читаемости — см. ниже
data-theme="dark-soft" dark 75 1 Стопка поднимается на ступень: переопределены ровно пять токенов
data-theme="dark" dark 75 1 Дно рампы, без переопределений

--tint — множитель, а не абсолютная величина: шаги рампы сохраняют свои доли уклона (у светлого конца он слабее, у тёмного сильнее), и при нуле весь ряд одновременно становится серым. Нейтральная тема — не третий тон, а его отсутствие: любой --hue-neutral при нулевой цветности даёт один и тот же серый.

<html data-theme="dark">   <!-- принудительно -->
<html>                     <!-- по системной настройке -->

Светлая глубина не регулируется. Сверху рампы шаги идут плотно, и «менее светлая» светлая тема даёт просто серый фон.

Потолок светло-серой

dark-light переопределяет больше пяти токенов, и это не исключение из правила «тема — это ручки», а его следствие: ручку глубины докрутили до упора, и дальше на неё отзывается всё, что от глубины зависело.

  • --surface-overlay совпадает с --surface-raised. Шаг --n-10 — последняя поверхность, на которой держится текст; на --n-9 даже --text-muted даёт 3.29 при норме 4.5. Это то же, что происходит на светлом конце, где raised и overlay оба сидят на --n-0: когда рампа кончается, слой над панелью отделяется рамкой и подложкой, а не светлотой.
  • Подписи статусов уходят на шаг 2. На --n-10 шаг 3 не дотягивает — зелёный бейдж давал 3.70.
  • Дорожка становится тёмной. Светлая альфа на приподнятой панели осветляет дорожку быстрее, чем заливку, и заливка тонет: бегунок давал 2.86 при норме 3.0.

Категориальная палитра

Пример
123456
Разметка
<div class="ramp">
  <span class="ramp-step" style="--c: var(--chart-1)"></span>
  <span class="ramp-step" style="--c: var(--chart-2)"></span>
  <span class="ramp-step" style="--c: var(--chart-3)"></span>
  <span class="ramp-step" style="--c: var(--chart-4)"></span>
  <span class="ramp-step" style="--c: var(--chart-5)"></span>
  <span class="ramp-step" style="--c: var(--chart-6)"></span>
</div>
<div class="ramp ramp-scale">
  <span>1</span><span>2</span><span>3</span><span>4</span><span>5</span><span>6</span>
</div>

Светлота внутри ряда разная. Так ряды различимы и при дальтонизме, и на чёрно-белой печати, где тон исчезает вовсе. Прищурьтесь — порядок по светлоте всё ещё читается.

Здесь цвет кодирует ряд, а не состояние, — единственное такое место в ките. Отсюда и отдельные правила.

Токен Тон Ряд
--chart-1 280° Первый
--chart-2 320° Второй
--chart-3 355° Третий
--chart-4 55° Четвёртый
--chart-5 115° Пятый
--chart-6 190° Шестой
  • Порядок — часть контракта. Ряд №1 всегда --chart-1, иначе один и тот же показатель меняет цвет от экрана к экрану.
  • Минимум 25° от каждого статусного тона (25, 85, 150) и от акцента (250): категориальный цвет не должен прочитаться как «упало».
  • Светлота внутри палитры разная намеренно — ряды остаются различимыми при дальтонизме и на чёрно-белой печати, где тон исчезает вовсе.
  • Шесть — потолок. Седьмой ряд означает, что это не график, а таблица.
  • Вне графика и его легенды запрещена. В роли статуса, заливки кнопки или подсветки строки категориального цвета не бывает.

Прочее

Токен Работа
--focus-ring Кольцо фокуса, одно на весь кит
--track Пустая дорожка меры и слайдера. Намеренно тихая: её работа — дать заливке отделиться, а не отделиться самой
--scrim Подложка модалки. В тёмной теме гуще. Затемнение, а не размытие
--shadow-color-near --shadow-color-far Цвета двух теней — см. высоту

Доступность

Проверка go -C tools run ./cmd/contrast читает настоящий tokens.css и резолвит light-dark(), color-mix() и var() так же, как браузер
Порог текста 4.5:1 во всех шести темах. --text-faint из этого исключён и потому не носит текст
Порог метки 3:1, но против двух фонов сразу: поверхности и дорожки. Отсюда --tone-mark отдельно от --tone-ink
Цвет не единственный носитель Статус несёт точку и слово, сноска несёт иконку, строка дифа несёт знак
Тёмная тема Не инверсия: контраст между соседними поверхностями расширяется, цветность опускается, рамки переходят с тёмной альфы на светлую
Режим принудительных цветов Носители значения переживают сброс через forced-color-adjust: none

API

ИмяЗначениеЧто делает
класс
inst-themeКорень области темы. Заново объявляет color и background, чтобы data-theme работал на поддереве
атрибут
data-themelight-neutral · light · light-cool · dark-light · dark-soft · darkТема. На корне документа или на любом поддереве вместе с inst-theme
токен
--text-primarylight-dark(var(--n-12), var(--n-1))Основной текст
--text-secondarylight-dark(var(--n-9), var(--n-5))Вторичный текст. Порог чтения держит
--text-mutedlight-dark(var(--n-8), var(--n-6))Подписи и вспомогательное
--text-faintlight-dark(var(--n-7), var(--n-7))Декорация. Текстом не красится
--surface-pagelight-dark(var(--n-1), var(--n-13))Фон страницы
--surface-sunkenlight-dark(var(--n-2), var(--n-14))Врез: утопленная поверхность
--surface-raisedlight-dark(var(--n-0), var(--n-12))Поднятая поверхность: панель, карточка
--surface-overlaylight-dark(var(--n-0), var(--n-11))Всплывающее: поповер, модалка
--surface-fieldlight-dark(var(--n-1), var(--n-14))Врез поля ввода
--surface-hoverlight-dark(oklch(0 0 0 / 0.035), oklch(1 0 0 / 0.045))Под курсором
--surface-activelight-dark(oklch(0 0 0 / 0.065), oklch(1 0 0 / 0.080))При нажатии
--surface-selectedcolor-mix(in oklab, var(--a-4) 14%, transparent)Выбранное
--borderlight-dark(oklch(0 0 0 / 0.12), oklch(1 0 0 / 0.11))Основная граница
--border-subtlelight-dark(oklch(0 0 0 / 0.07), oklch(1 0 0 / 0.06))Разделитель внутри блока
--border-stronglight-dark(oklch(0 0 0 / 0.22), oklch(1 0 0 / 0.20))Усиленная граница
--border-controllight-dark(oklch(0 0 0 / 0.46), oklch(1 0 0 / 0.38))Граница контрола. Держит 3:1
--accent-textlight-dark(var(--a-5), var(--a-3))Акцент для текста, 4.5:1
--accent-marklight-dark(var(--a-4), var(--a-3))Акцент для метки без подписи, 3:1
--accent-solidvar(--a-4)Сплошная заливка акцентом
--accent-onvar(--n-0)Передний план на сплошном акценте
--accent-bglight-dark(var(--a-1), color-mix(in oklab, var(--a-4) 15%, transparent))Тонированный фон акцента
--accent-borderlight-dark(var(--a-4), var(--a-3))Граница акцентом
--tone-inkvar(--accent-text)Передний план тона для ТЕКСТА, 4.5:1
--tone-markvar(--text-secondary)Передний план тона для МЕТКИ, 3:1
--tone-bgvar(--surface-sunken)Тонированный фон
--ok-textlight-dark(var(--ok-5), var(--ok-3))Тон успеха
--warn-textlight-dark(var(--warn-5), var(--warn-3))Тон замечания
--err-textlight-dark(var(--err-5), var(--err-3))Тон отказа
--focus-ringlight-dark(var(--a-4), var(--a-3))Кольцо фокуса
--scrimlight-dark(oklch(0 0 0 / 0.32), oklch(0 0 0 / 0.58))Подложка модалки
--tracklight-dark(oklch(0 0 0 / 0.10), oklch(1 0 0 / 0.16))Дорожка меры, слайдера, кольца

Почему кит устроен именно так — конституция · Открытый код под MIT, github.com/keshon/instrument