Основания
Токены
Полный справочник. Четыре яруса, и разделение между ними — весь смысл файла: компонент имеет право видеть ярусы 2 и 3, ярус 1 — никогда.
- 1. Рампы
- Сырые шаги. Не меняются между темами
- 2. Семантика
- Для чего цвет нужен. Одно объявление через light-dark()
- 3. Роли
- Отступы и размеры по назначению. Их перенастраивает плотность
- 4. Компонент
- --btn-bg и подобные, 2–3 переменные
Разметка
<div class="inst-panel">
<div class="inst-panel-header">
<span class="inst-panel-title">Ярусы</span>
</div>
<div class="inst-panel-body">
<dl class="inst-kv">
<dt>1. Рампы</dt><dd>Сырые шаги. Не меняются между темами</dd>
<dt>2. Семантика</dt><dd>Для чего цвет нужен. Одно объявление через light-dark()</dd>
<dt>3. Роли</dt><dd>Отступы и размеры по назначению. Их перенастраивает плотность</dd>
<dt>4. Компонент</dt><dd>--btn-bg и подобные, 2–3 переменные</dd>
</dl>
</div>
</div>
Использование
Компонент видит ярусы 2 и 3 — семантику и роли. Ярус 1 не видит никогда.
.my-thing {
/* ярус 2: для чего цвет нужен */
color: var(--text-primary);
/* ярус 3: отступ по назначению */
padding: var(--pad-panel);
/* ярус 4: точка настройки самого компонента.
У кнопки таких пять: --btn-bg и соседи */
background: var(--btn-bg);
}
| Что | Обязательно | Почему |
|---|---|---|
| Ярусы 2 и 3 в компоненте | да | Ярус 1 — сырьё: рампа между темами не меняется, и обращение к ней захардкоживает светлую тему |
| Один экземпляр каждого параметра | да | Дубликат — это будущее расхождение, и оно наступает быстрее, чем кажется |
| 2–3 переменные на компонент | да | Ярус 4 — точки настройки, а не копия всех свойств |
Значение через light-dark() |
да | Второй темы как отдельного блока не существует |
Когда использовать
| Используйте | Возьмите другое |
|---|---|
Ярус 2 — семантика: --text-primary, --surface-raised, --ok-text |
Ярус 1 (--n-3, --a-4) — это захардкоженная светлая тема. Разбор рамп — в цвете |
Ярус 3 — роли: --pad-panel, --control-h-md, --size-dot |
Число в компоненте — оно сломает плотность, см. плотность |
| Ярус 4 — 2–3 переменные компонента для варианта | Копию всего блока правил — вариант должен быть двумя строками, как у кнопки |
| Новый параметр — как роль | Новую константу — константа переживёт вас и сломает compact. Шкалы — в ритме и форме |
| Переопределение роли в приложении | !important — роли объявлены через :where(), борьбы не будет. Пример — форма |
Устройство
Сырые шаги. Между темами не меняются. Рампа непрерывна по определению: шаг, не занятый семантикой, — резерв под приложение и графики, а не мусор.
| Группа | Токены | Тон |
|---|---|---|
| Нейтраль | --n-0 … --n-14 |
--hue-neutral (75 тёплый / 250 холодный) |
| Акцент | --a-1 … --a-6 |
250° |
| Успех | --ok-1 --ok-2 --ok-3 --ok-4 --ok-5 --ok-6 |
150° |
| Предупреждение | --warn-1 --warn-3 --warn-4 --warn-5 --warn-6 |
85° |
| Ошибка | --err-1 --err-3 --err-4 --err-5 --err-6 |
25° |
Шага 2 у статусов нет: у них закреплённые роли (1 — фон в светлой, 3 — текст в тёмной, 4 — заливка в светлой, 5 — текст в светлой, 6 — резерв под графики), и свободный шаг между 1 и 3 не понадобился ни разу. Значения светлот и разбор ролей — в цвете.
Ручка тона
| Токен | Значение | Что делает |
|---|---|---|
--hue-neutral |
75 | Уклон нейтрали. 75 — тёплый, 250 — холодный. Цветность 0.002–0.006: ниже порога осознанного замечания |
Ярус 2 — семантика
Одно объявление на токен через light-dark(). Второго блока токенов не
существует, поэтому и расходиться нечему.
Поверхности
| Токен | Светлая | Тёмная |
|---|---|---|
--surface-sunken |
--n-2 |
--n-14 |
--surface-page |
--n-1 |
--n-13 |
--surface-raised |
--n-0 |
--n-12 |
--surface-overlay |
--n-0 |
--n-11 |
--surface-field |
--n-1 |
--n-14 |
--surface-hover |
чёрный 3.5% | белый 4.5% |
--surface-active |
чёрный 6.5% | белый 8% |
--surface-selected |
--a-4 14%, в обеих темах |
Текст
| Токен | Светлая | Тёмная | Порог |
|---|---|---|---|
--text-primary |
--n-12 |
--n-1 |
4.5:1 |
--text-secondary |
--n-9 |
--n-5 |
4.5:1 |
--text-muted |
--n-8 |
--n-6 |
4.5:1 |
--text-faint |
--n-7 |
--n-7 |
3:1 — не для текста, который читают |
Рамки
| Токен | Светлая | Тёмная | Требование |
|---|---|---|---|
--border-subtle |
чёрный 7% | белый 6% | Декоративная |
--border |
чёрный 12% | белый 11% | Декоративная |
--border-strong |
чёрный 22% | белый 20% | Декоративная |
--border-control |
чёрный 46% | белый 38% | Несущая, 3:1 |
Акцент
| Токен | Светлая | Тёмная | Работа |
|---|---|---|---|
--accent-text |
--a-5 |
--a-3 |
Текст, 4.5:1 |
--accent-mark |
--a-4 |
--a-3 |
Метка без подписи, 3:1 против поверхности и дорожки |
--accent-bg |
--a-1 |
--a-4 15% |
Тонированный фон |
--accent-solid |
--a-4, в обеих темах |
Заливка кнопки, потолок светлоты 0.56 | |
--accent-on |
--n-0, в обеих темах |
Подпись на заливке | |
--accent-hover |
--a-5, в обеих темах |
Уходит от цвета подписи, а не к нему | |
--accent-border |
--a-4 |
--a-3 |
Рамка акцентированного контрола |
--focus-ring |
--a-4 |
--a-3 |
Кольцо фокуса |
Статусы
| Токен | Светлая | Тёмная |
|---|---|---|
--ok-text |
--ok-5 |
--ok-3 |
--ok-bg |
--ok-1 |
--ok-4 16% |
--warn-text |
--warn-5 |
--warn-3 |
--warn-bg |
--warn-1 |
--warn-4 16% |
--err-text |
--err-5 |
--err-3 |
--err-bg |
--err-1 |
--err-4 16% |
Третьего токена — «заливки» между текстом и фоном — нет. Исключение одно, акцент: у него есть случай, которого нет у статусов, — заливка под белой подписью.
Тон через атрибут
data-tone ставит три переменные, компонент читает их и больше ничего о тоне
не знает.
| Значение | --tone-ink |
--tone-mark |
--tone-bg |
|---|---|---|---|
neutral |
--text-secondary |
--text-secondary |
--surface-sunken |
running |
--accent-text |
--accent-mark |
--accent-bg |
ok |
--ok-text |
--ok-text |
--ok-bg |
warn |
--warn-text |
--warn-text |
--warn-bg |
error |
--err-text |
--err-text |
--err-bg |
Графики
| Токен | Тон | Светлая | Тёмная |
|---|---|---|---|
--chart-1 |
280° | L 0.520 | L 0.700 |
--chart-2 |
320° | L 0.600 | L 0.760 |
--chart-3 |
355° | L 0.550 | L 0.720 |
--chart-4 |
55° | L 0.650 | L 0.800 |
--chart-5 |
115° | L 0.580 | L 0.740 |
--chart-6 |
190° | L 0.620 | L 0.780 |
Прочее
| Токен | Светлая | Тёмная |
|---|---|---|
--track |
чёрный 10% | белый 16% |
--scrim |
чёрный 32% | чёрный 58% |
--shadow-color-near |
чёрный 6% | чёрный 30% |
--shadow-color-far |
чёрный 10% | чёрный 45% |
--shadow-popover |
двухслойная, короткая | |
--shadow-modal |
двухслойная, длинная |
Порядок слоёв
| Токен | Значение |
|---|---|
--z-sticky |
10 |
--z-popover |
100 |
--z-modal |
1000 |
Ярус 3 — роли
Единственный ярус, который перенастраивает плотность, и единственная причина,
по которой в компонентах нет констант. Объявлен через :where() — нулевая
специфичность, приложение переопределяет без борьбы. Значения ниже — для
плотности по умолчанию, остальные две — в плотности.
Высоты и отступы контролов
| Токен | Значение |
|---|---|
--control-h-xs |
20px |
--control-h-sm |
26px |
--control-h-md |
32px |
--control-h-lg |
38px |
--control-pad-sm |
--space-4 |
--control-pad-md |
--space-5 |
--control-pad-lg |
--space-6 |
Отступы по назначению
| Токен | Значение |
|---|---|
--pad-panel |
--space-5 |
--pad-card |
--space-6 |
--pad-cell-x |
--space-4 |
--row-pad-y |
--space-3 |
--gap-row |
--space-3 |
--gap-inline |
--space-4 |
Глифы и жёлобы
| Токен | Значение |
|---|---|
--size-dot |
6px |
--size-check |
15px |
--size-switch-w |
30px |
--size-switch-h |
18px |
--size-thumb |
14px |
--size-track |
4px |
--size-meter |
6px |
--size-spinner |
13px |
--size-icon |
16px |
--size-chevron |
10px |
--size-marker |
2px |
--size-gutter |
14px |
--size-indent |
14px |
--label-col |
92px |
Раскладка
| Токен | Значение |
|---|---|
--aside-w |
232px |
--container-max |
76rem |
--split-side |
18rem |
--split-main |
30rem |
--col-min |
260px |
Шкала
Не привязаны к теме и не перенастраиваются плотностью. Подробности — в типографике и ритме и форме.
Типографика
| Группа | Токены |
|---|---|
| Семейства | --font-sans --font-mono |
| Кегли | --text-2xs --text-xs --text-sm --text-md --text-lg --text-xl --text-2xl |
| Начертания | --weight-normal --weight-medium |
| Интерлиньяж | --leading-tight --leading-ui --leading-prose |
| Трекинг | --tracking-tight --tracking-normal |
Пространство и форма
| Группа | Токены |
|---|---|
| Шкала | --space-1 … --space-10 (2 · 4 · 6 · 8 · 12 · 16 · 24 · 32 · 48 · 64) |
| Радиусы | --radius-xs 3 · --radius-sm 5 · --radius-md 7 · --radius-lg 11 · --radius-full |
| Линии | --hairline (0.5px от 1.5dppx, иначе 1px) · --stroke 1px |
Движение
| Группа | Токены |
|---|---|
| Длительности | --dur-1 80ms · --dur-2 140ms · --dur-3 220ms |
| Плавность | --ease-out · --ease-in-out |
Ярус 4 — компонент
Живёт не здесь, а рядом с компонентом. Правило одно: 2–3 переменные, не больше, и вариант описывается ими, а не переписыванием блока.
.my-btn--accent {
--btn-fg: var(--accent-on);
--btn-bg: var(--accent-solid);
}
Список переменных конкретного компонента — на его странице: например, у кнопки их пять, у таблицы — свои.
Варианты
| Атрибут | color-scheme |
--hue-neutral |
Ещё |
|---|---|---|---|
| нет | по системе | 75 | |
data-theme="light-neutral" |
light |
— | --tint: 0 — уклон выключен |
data-theme="light" |
light |
75 | |
data-theme="light-cool" |
light |
250 | |
data-theme="dark-light" |
dark |
75 | Стопка на две ступени вверх, плюс подписи статусов и дорожка |
data-theme="dark-soft" |
dark |
75 | Переопределены --surface-sunken --surface-page --surface-raised --surface-overlay --surface-field |
data-theme="dark" |
dark |
75 |
Правила
| Правило | Следствие |
|---|---|
| Компонент видит ярусы 2 и 3 | --n-3 в компоненте — захардкоженная светлая тема; 14px — сломанная плотность |
| У смыслового тона ровно два токена | --*-text (метка) и --*-bg (фон). Третий не проходил порог на дорожке |
| Текст и метка — разные передние планы | --tone-ink держит 4.5:1, --tone-mark — 3:1, но против двух фонов |
| Несущая и декоративная рамка — разные токены | --border-control обязан 3:1, --border — нет |
Дефолты объявляются через :where() |
Дефолт с той же специфичностью, что переопределение, — не дефолт |
| Новое значение — роль, а не константа | Иначе оно не переживёт compact |
| Второго акцентного тона не бывает | Это не запрос фичи, а сигнал, что смысл несёт что-то другое |
Доступность
| Проверка | go -C tools run ./cmd/contrast читает настоящий tokens.css и резолвит light-dark(), color-mix() и var() так же, как браузер. Новая пара токенов добавляется туда вместе с токеном |
| Пороги | Текст 4.5:1, метка 3:1 (против поверхности и против дорожки), несущая рамка 3:1 |
| Шесть тем | Каждая пара проверяется во всех шести, а не в двух |
Кегли в rem |
Настройка «размер шрифта по умолчанию» в браузере работает; высоты применяются как min-block-size |
| Пол кегля | --text-2xs, 11px. Ниже токена нет |
API
Перечисляет генератор, читая src/tokens.css. Список не пишется руками и
потому не может отстать от кита.
| Имя | Значение | Что делает |
|---|---|---|
| токен | ||
--a-1 | oklch(0.960 0.022 250) | |
--a-2 | oklch(0.800 0.100 250) | |
--a-3 | oklch(0.700 0.130 250) | |
--a-4 | oklch(0.560 0.155 250) | |
--a-5 | oklch(0.450 0.135 250) | |
--a-6 | oklch(0.330 0.100 250) | |
--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)) | |
--accent-hover | var(--a-5) | |
--accent-mark | light-dark(var(--a-4), var(--a-3)) | |
--accent-on | var(--n-0) | |
--accent-solid | var(--a-4) | |
--accent-text | light-dark(var(--a-5), var(--a-3)) | |
--aside-w | 232px | |
--border | light-dark(oklch(0 0 0 / 0.12), oklch(1 0 0 / 0.11)) | |
--border-control | light-dark(oklch(0 0 0 / 0.46), oklch(1 0 0 / 0.38)) | |
--border-strong | light-dark(oklch(0 0 0 / 0.22), oklch(1 0 0 / 0.20)) | |
--border-subtle | light-dark(oklch(0 0 0 / 0.07), oklch(1 0 0 / 0.06)) | |
--chart-1 | light-dark(oklch(0.520 0.150 280), oklch(0.700 0.130 280)) | |
--chart-2 | light-dark(oklch(0.600 0.150 320), oklch(0.760 0.120 320)) | |
--chart-3 | light-dark(oklch(0.550 0.140 355), oklch(0.720 0.120 355)) | |
--chart-4 | light-dark(oklch(0.650 0.120 55), oklch(0.800 0.110 55)) | |
--chart-5 | light-dark(oklch(0.580 0.130 115), oklch(0.740 0.120 115)) | |
--chart-6 | light-dark(oklch(0.620 0.110 190), oklch(0.780 0.100 190)) | |
--col-min | 260px | |
--container-max | 76rem | |
--control-h-lg | 38px | |
--control-h-md | 32px | |
--control-h-sm | 26px | |
--control-h-xs | 20px | |
--control-pad-lg | var(--space-6) | |
--control-pad-md | var(--space-5) | |
--control-pad-sm | var(--space-4) | |
--dur-1 | 80ms | |
--dur-2 | 140ms | |
--dur-3 | 220ms | |
--ease-in-out | cubic-bezier(0.65, 0, 0.35, 1) | |
--ease-out | cubic-bezier(0.22, 0.61, 0.36, 1) | |
--err-1 | oklch(0.955 0.025 25) | |
--err-2 | oklch(0.760 0.150 25) | |
--err-3 | oklch(0.725 0.160 25) | |
--err-4 | oklch(0.545 0.185 25) | |
--err-5 | oklch(0.500 0.185 25) | |
--err-6 | oklch(0.330 0.110 25) | |
--err-bg | light-dark(var(--err-1), color-mix(in oklab, var(--err-4) 16%, transparent)) | |
--err-mark | light-dark(var(--err-4), var(--err-3)) | |
--err-text | light-dark(var(--err-5), var(--err-3)) | |
--focus-ring | light-dark(var(--a-4), var(--a-3)) | |
--font-mono | ui-monospace, "Cascadia Code", "JetBrains Mono", "SF Mono", Consolas, "Liberation Mono", monospace | |
--font-sans | ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif | |
--gap-inline | var(--space-4) | |
--gap-row | var(--space-3) | |
--hairline | 1px | |
--hue-neutral | 75 | |
--label-col | 92px | |
--leading-prose | 1.65 | |
--leading-tight | 1.25 | |
--leading-ui | 1.4 | |
--n-0 | oklch(0.994 calc(0.002 * var(--tint)) var(--hue-neutral)) | |
--n-1 | oklch(0.978 calc(0.003 * var(--tint)) var(--hue-neutral)) | |
--n-10 | oklch(0.322 calc(0.005 * var(--tint)) var(--hue-neutral)) | |
--n-11 | oklch(0.242 calc(0.004 * var(--tint)) var(--hue-neutral)) | |
--n-12 | oklch(0.196 calc(0.004 * var(--tint)) var(--hue-neutral)) | |
--n-13 | oklch(0.155 calc(0.003 * var(--tint)) var(--hue-neutral)) | |
--n-14 | oklch(0.120 calc(0.003 * var(--tint)) var(--hue-neutral)) | |
--n-2 | oklch(0.958 calc(0.004 * var(--tint)) var(--hue-neutral)) | |
--n-3 | oklch(0.928 calc(0.004 * var(--tint)) var(--hue-neutral)) | |
--n-4 | oklch(0.884 calc(0.005 * var(--tint)) var(--hue-neutral)) | |
--n-5 | oklch(0.806 calc(0.006 * var(--tint)) var(--hue-neutral)) | |
--n-6 | oklch(0.706 calc(0.006 * var(--tint)) var(--hue-neutral)) | |
--n-7 | oklch(0.606 calc(0.006 * var(--tint)) var(--hue-neutral)) | |
--n-8 | oklch(0.508 calc(0.006 * var(--tint)) var(--hue-neutral)) | |
--n-9 | oklch(0.416 calc(0.006 * var(--tint)) var(--hue-neutral)) | |
--ok-1 | oklch(0.955 0.025 150) | |
--ok-2 | oklch(0.780 0.140 150) | |
--ok-3 | oklch(0.660 0.130 150) | |
--ok-4 | oklch(0.545 0.120 150) | |
--ok-5 | oklch(0.500 0.120 150) | |
--ok-6 | oklch(0.330 0.075 150) | |
--ok-bg | light-dark(var(--ok-1), color-mix(in oklab, var(--ok-4) 16%, transparent)) | |
--ok-mark | light-dark(var(--ok-4), var(--ok-3)) | |
--ok-text | light-dark(var(--ok-5), var(--ok-3)) | |
--pad-card | var(--space-6) | |
--pad-cell-x | var(--space-4) | |
--pad-panel | var(--space-5) | |
--radius-full | 999px | |
--radius-lg | 11px | |
--radius-md | 7px | |
--radius-sm | 5px | |
--radius-xs | 3px | |
--row-pad-y | var(--space-3) | |
--scrim | light-dark(oklch(0 0 0 / 0.32), oklch(0 0 0 / 0.58)) | |
--shadow-color-far | light-dark(oklch(0 0 0 / 0.10), oklch(0 0 0 / 0.45)) | |
--shadow-color-near | light-dark(oklch(0 0 0 / 0.06), oklch(0 0 0 / 0.30)) | |
--shadow-modal | 0 16px 40px -8px var(--shadow-color-far), 0 4px 10px -4px var(--shadow-color-near) | |
--shadow-popover | 0 4px 12px -2px var(--shadow-color-far), 0 2px 4px -2px var(--shadow-color-near) | |
--size-check | 15px | |
--size-chevron | 10px | |
--size-dot | 6px | |
--size-gutter | 14px | |
--size-icon | 16px | |
--size-indent | 14px | |
--size-marker | 2px | |
--size-meter | 6px | |
--size-spinner | 13px | |
--size-switch-h | 18px | |
--size-switch-w | 30px | |
--size-thumb | 14px | |
--size-tick | 3px | |
--size-track | 4px | |
--space-1 | 2px | |
--space-10 | 64px | |
--space-2 | 4px | |
--space-3 | 6px | |
--space-4 | 8px | |
--space-5 | 12px | |
--space-6 | 16px | |
--space-7 | 24px | |
--space-8 | 32px | |
--space-9 | 48px | |
--split-main | 30rem | |
--split-side | 18rem | |
--stroke | 1px | |
--surface-active | light-dark(oklch(0 0 0 / 0.065), oklch(1 0 0 / 0.080)) | |
--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-overlay | light-dark(var(--n-0), var(--n-11)) | |
--surface-page | light-dark(var(--n-1), var(--n-13)) | |
--surface-raised | light-dark(var(--n-0), var(--n-12)) | |
--surface-selected | color-mix(in oklab, var(--a-4) 14%, transparent) | |
--surface-sunken | light-dark(var(--n-2), var(--n-14)) | |
--tap-min | 24px | |
--text-2xl | 1.6875rem | |
--text-2xs | 0.6875rem | |
--text-faint | light-dark(var(--n-7), var(--n-7)) | |
--text-lg | 1.0625rem | |
--text-md | 0.9375rem | |
--text-muted | light-dark(var(--n-8), var(--n-6)) | |
--text-primary | light-dark(var(--n-12), var(--n-1)) | |
--text-secondary | light-dark(var(--n-9), var(--n-5)) | |
--text-sm | 0.8125rem | |
--text-xl | 1.3125rem | |
--text-xs | 0.75rem | |
--tint | 1 | |
--track | light-dark(oklch(0 0 0 / 0.10), oklch(1 0 0 / 0.16)) | |
--tracking-normal | 0 | |
--tracking-tight | -0.01em | |
--warn-1 | oklch(0.960 0.040 85) | |
--warn-2 | oklch(0.860 0.145 85) | |
--warn-3 | oklch(0.760 0.140 85) | |
--warn-4 | oklch(0.620 0.135 85) | |
--warn-5 | oklch(0.520 0.135 85) | |
--warn-6 | oklch(0.360 0.080 85) | |
--warn-bg | light-dark(var(--warn-1), color-mix(in oklab, var(--warn-4) 16%, transparent)) | |
--warn-mark | light-dark(var(--warn-4), var(--warn-3)) | |
--warn-text | light-dark(var(--warn-5), var(--warn-3)) | |
--weight-medium | 500 | |
--weight-normal | 400 | |
--z-modal | 1000 | |
--z-popover | 100 | |
--z-sticky | 10 | |
Почему кит устроен именно так — конституция · Открытый код под MIT, github.com/keshon/instrument