Основания
Цвет
Два яруса. Внизу рампы — сырые шаги, они не меняются никогда. Сверху
семантика — для чего цвет нужен. Компонент видит только семантику: тот, кто
написал --n-3, только что захардкодил светлую тему.
Разметка
<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 шагов
Разметка
<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 (тёмная) |
Шаги без семантики — резерв под приложение и графики.
Акцент — один тон
Разметка
<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.
Статусы — три зарезервированных тона
Разметка
<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(). Второй темы как отдельного блока
не существует — значит, и расходиться нечему.
Поверхности
Разметка
<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.
Категориальная палитра
Разметка
<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-theme | light-neutral · light · light-cool · dark-light · dark-soft · dark | Тема. На корне документа или на любом поддереве вместе с inst-theme |
| токен | ||
--text-primary | light-dark(var(--n-12), var(--n-1)) | Основной текст |
--text-secondary | light-dark(var(--n-9), var(--n-5)) | Вторичный текст. Порог чтения держит |
--text-muted | light-dark(var(--n-8), var(--n-6)) | Подписи и вспомогательное |
--text-faint | light-dark(var(--n-7), var(--n-7)) | Декорация. Текстом не красится |
--surface-page | light-dark(var(--n-1), var(--n-13)) | Фон страницы |
--surface-sunken | light-dark(var(--n-2), var(--n-14)) | Врез: утопленная поверхность |
--surface-raised | light-dark(var(--n-0), var(--n-12)) | Поднятая поверхность: панель, карточка |
--surface-overlay | light-dark(var(--n-0), var(--n-11)) | Всплывающее: поповер, модалка |
--surface-field | light-dark(var(--n-1), var(--n-14)) | Врез поля ввода |
--surface-hover | light-dark(oklch(0 0 0 / 0.035), oklch(1 0 0 / 0.045)) | Под курсором |
--surface-active | light-dark(oklch(0 0 0 / 0.065), oklch(1 0 0 / 0.080)) | При нажатии |
--surface-selected | color-mix(in oklab, var(--a-4) 14%, transparent) | Выбранное |
--border | light-dark(oklch(0 0 0 / 0.12), oklch(1 0 0 / 0.11)) | Основная граница |
--border-subtle | light-dark(oklch(0 0 0 / 0.07), oklch(1 0 0 / 0.06)) | Разделитель внутри блока |
--border-strong | light-dark(oklch(0 0 0 / 0.22), oklch(1 0 0 / 0.20)) | Усиленная граница |
--border-control | light-dark(oklch(0 0 0 / 0.46), oklch(1 0 0 / 0.38)) | Граница контрола. Держит 3:1 |
--accent-text | light-dark(var(--a-5), var(--a-3)) | Акцент для текста, 4.5:1 |
--accent-mark | light-dark(var(--a-4), var(--a-3)) | Акцент для метки без подписи, 3:1 |
--accent-solid | var(--a-4) | Сплошная заливка акцентом |
--accent-on | var(--n-0) | Передний план на сплошном акценте |
--accent-bg | light-dark(var(--a-1), color-mix(in oklab, var(--a-4) 15%, transparent)) | Тонированный фон акцента |
--accent-border | light-dark(var(--a-4), var(--a-3)) | Граница акцентом |
--tone-ink | var(--accent-text) | Передний план тона для ТЕКСТА, 4.5:1 |
--tone-mark | var(--text-secondary) | Передний план тона для МЕТКИ, 3:1 |
--tone-bg | var(--surface-sunken) | Тонированный фон |
--ok-text | light-dark(var(--ok-5), var(--ok-3)) | Тон успеха |
--warn-text | light-dark(var(--warn-5), var(--warn-3)) | Тон замечания |
--err-text | light-dark(var(--err-5), var(--err-3)) | Тон отказа |
--focus-ring | light-dark(var(--a-4), var(--a-3)) | Кольцо фокуса |
--scrim | light-dark(oklch(0 0 0 / 0.32), oklch(0 0 0 / 0.58)) | Подложка модалки |
--track | light-dark(oklch(0 0 0 / 0.10), oklch(1 0 0 / 0.16)) | Дорожка меры, слайдера, кольца |
Почему кит устроен именно так — конституция · Открытый код под MIT, github.com/keshon/instrument