Основания
Высота и поверхности
Глубину передаёт порядок светлоты. Тень означает здесь одно — «плавает сверху и сейчас исчезнет», — и носителей у неё два: поповер и модалка.
Разметка
<div class="inst-panel">
<div class="inst-panel-header">
<span class="inst-panel-title">Прогон #4127</span>
</div>
<div class="inst-panel-body inst-stack">
<div class="inst-card">
<div class="inst-card-title">worldgen-01</div>
<div class="inst-card-sub">Карточка на панели: ступень поверхности, ни одной тени</div>
</div>
<input class="inst-input" type="text" value="terrain_chunk_04">
</div>
</div>
Использование
Глубину передаёт поверхность. Тень означает «плавает сверху и сейчас исчезнет» и принадлежит трём компонентам.
.my-panel {
background: var(--surface-raised);
border: var(--hairline) solid var(--border);
}
| Что | Обязательно | Почему |
|---|---|---|
| Поверхность из семантики | да | Четыре ступени названы работой, а не номером: страница, врез, поднятая, оверлей |
| Тень только у всплывающего | да | Поповер, модалка, уведомление. Четвёртого носителя тени в ките нет |
Рамка CanvasText в forced-colors |
да | Поверхности в этом режиме схлопываются, и без рамки блок сливается с фоном |
z-index из словаря |
да | Четыре токена. Число, выдуманное на месте, работает до первого чужого числа побольше |
Когда использовать
| Используйте | Возьмите другое |
|---|---|
| Перепад поверхностей для постоянной структуры | Тень у карточки или панели — тень означает «временное». Структура строится карточкой и панелью |
--shadow-popover для того, что исчезнет по Escape |
Тень «для объёма» — у неё нет получателя, кроме поповера |
--shadow-modal + --scrim для того, что блокирует экран |
Поповер вместо модалки, если решение обязательно — берите модалку |
--surface-field под ввод |
--surface-raised под ввод — поле сольётся с панелью, см. поле |
| Размытие фона под оверлеем | Размытий кит не знает вовсе — затемнение делает --scrim |
Устройство
Объявлен в одном месте, чтобы не разъезжался по компонентам.
| Токен | Значение | Кто |
|---|---|---|
--z-sticky |
10 | Липкая шапка таблицы, полоса действий формы |
--z-popover |
100 | Поповер, меню, тултип |
--z-modal |
1000 | Модалка, шторка |
Поповер и модалка на Popover API и <dialog> живут в верхнем слое браузера, где
z-index уже не решает. Токены остаются для того, что верхнего слоя не
получает, — и для приложения, которому нужно встроиться в тот же порядок.
Шкала
Читаются как стопка снизу вверх. Ступень выбирается по роли области, а не по желаемой яркости.
| Токен | Ступень | Кто носит |
|---|---|---|
--surface-sunken |
Дно | Дорожка, шапка таблицы, блок кода, тело шага, фон бокового столбца |
--surface-page |
Страница | body, фон рабочей области |
--surface-raised |
Поднято | Панель, карточка, шапка оболочки, полоса действий формы, <kbd> |
--surface-overlay |
Оверлей | Поповер, меню, тултип, модалка, шторка |
Пятая роль стоит вне стопки, потому что она не про глубину, а про ввод:
| Токен | Работа |
|---|---|
--surface-field |
Врез под поле. Отдельная роль, а не переиспользование raised: пока поле красилось цветом панели, его единственной границей была рамка на 1.31:1 — то есть поля на панели не было видно вообще |
В тёмной теме между соседними ступенями расстояние шире, чем в светлой:
тёмный конец рампы различается хуже, и зеркальная светлая раскладка дала бы
четыре одинаковых чёрных прямоугольника. В теме dark-soft вся стопка
поднимается на шаг рампы — переопределяются ровно пять токенов, а остальная
семантика не знает, что тема сменилась. В dark-light она поднимается на две
ступени и упирается в потолок: overlay там совпадает с raised, потому что
следующий шаг рампы уже не держит текст. Ровно так же ведёт себя светлый конец,
где обе роли сидят на --n-0.
Состояния поверх поверхности
Наведение и выделение — альфа. Непрозрачная заливка не давала бы им складываться: наведение по выделенной строке пропадало бы.
| Токен | Работа |
|---|---|
--surface-hover |
Наведение |
--surface-active |
Нажатие |
--surface-selected |
Выделенная строка |
Варианты
Обе функциональны. Обе объявлены целиком в токенах — компонент подставляет готовое значение и не собирает тень сам.
| Токен | Значение | Кто носит |
|---|---|---|
--shadow-popover |
0 4px 12px -2px дальняя + 0 2px 4px -2px ближняя |
Поповер, меню, тултип |
--shadow-modal |
0 16px 40px -8px дальняя + 0 4px 10px -4px ближняя |
Модалка, шторка |
Каждая тень двухслойная: дальняя даёт расстояние до фона, ближняя — контакт края. Одним слоем это выглядит либо приклеенным, либо парящим без опоры.
Цвет тени вынесен отдельно, потому что light-dark() принимает только <color>
и целое значение тени через неё не проходит:
| Токен | Светлая | Тёмная |
|---|---|---|
--shadow-color-near |
oklch(0 0 0 / 0.06) |
oklch(0 0 0 / 0.30) |
--shadow-color-far |
oklch(0 0 0 / 0.10) |
oklch(0 0 0 / 0.45) |
В тёмной теме тень гуще. Слабая тень на тёмном фоне невидима, а светлая тень — это уже свечение, которого кит не знает.
Правило
Тень получает только то, что появилось по действию и исчезнет по Escape. Всё, что стоит на экране постоянно, отделяется поверхностью и рамкой. Карточка не получает тень — она получает поверхность.
| Элемент | Тень |
|---|---|
| Панель, карточка, таблица, полоса действий | Нет |
| Поповер, меню, тултип | --shadow-popover |
| Модалка, шторка | --shadow-modal |
| Всё остальное | Нет, и нового получателя не заводится |
Подложка
| Токен | Светлая | Тёмная |
|---|---|---|
--scrim |
oklch(0 0 0 / 0.32) |
oklch(0 0 0 / 0.58) |
В тёмной теме подложка гуще: на светлой странице затемнение само по себе создаёт контраст, на тёмной его приходится добавлять. Это затемнение, а не размытие — градиенты, свечения и размытия кит запрещает целиком.
Подложка ставится на ::backdrop нативного <dialog>, поэтому верхний слой,
захват фокуса и закрытие по Escape приходят от платформы, а не от кита.
Доступность
| Глубина не только цветом | Каждая ступень поверхности сопровождается рамкой или перепадом ритма: различение стопки не требует различения светлот |
| Контраст ступеней | Соседние поверхности проверены как пара во всех шести темах — иначе поле на панели пропадает |
| Тень не несёт смысла в одиночку | Оверлей опознаётся ролью и поведением (popover, <dialog>), а не тенью. В режиме принудительных цветов тень исчезает, и это ничего не ломает |
| Прокрутка под модалкой | Останавливается: showModal() делает фон инертным для кликов, но не для колеса, и пользователь терял место, к которому вернётся |
| Уменьшенное движение | Появление оверлея схлопывается до 0.01ms, но не выключается — машины состояний, слушающие transitionend, продолжают работать. См. движение |
API
| Имя | Значение | Что делает |
|---|---|---|
| токен | ||
--surface-sunken | light-dark(var(--n-2), var(--n-14)) | |
--surface-page | light-dark(var(--n-1), var(--n-13)) | |
--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) | |
--shadow-popover | 0 4px 12px -2px var(--shadow-color-far), 0 2px 4px -2px var(--shadow-color-near) | |
--shadow-modal | 0 16px 40px -8px var(--shadow-color-far), 0 4px 10px -4px var(--shadow-color-near) | |
--shadow-color-near | light-dark(oklch(0 0 0 / 0.06), oklch(0 0 0 / 0.30)) | |
--shadow-color-far | light-dark(oklch(0 0 0 / 0.10), oklch(0 0 0 / 0.45)) | |
--scrim | light-dark(oklch(0 0 0 / 0.32), oklch(0 0 0 / 0.58)) | |
--z-sticky | 10 | |
--z-popover | 100 | |
--z-modal | 1000 | |
--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)) | |
--hairline | 1px | |
Почему кит устроен именно так — конституция · Открытый код под MIT, github.com/keshon/instrument