Основания
Типографика
Семь ступеней и два начертания. База документа — интерфейс, а не проза: 14/1.4, потому что подписи, ячейки, кнопки и строки задач — частый случай.
Разметка
<div class="inst-stack inst-stack--tight">
<span style="font-size:var(--text-2xl)">2xl · 28px · число-герой</span>
<span style="font-size:var(--text-xl)">xl · 22px · заголовок раздела</span>
<span style="font-size:var(--text-lg)">lg · 18px · заголовок блока</span>
<span style="font-size:var(--text-md)">md · 16px · проза, имя панели</span>
<span style="font-size:var(--text-sm)">sm · 14px · база интерфейса</span>
<span style="font-size:var(--text-xs)">xs · 12.5px · время, метаданные</span>
<span style="font-size:var(--text-2xs)">2xs · 11px · бейдж, подпись оси</span>
</div>
Ступень существует, только если её видно. Отношения соседних кеглей — 1.222 · 1.273 в дисплейной части и 1.136 · 1.12 · 1.143 · 1.125 в рабочей: ниже этого глаз перестаёт различать размеры, и три ступени начинают работать как одна.
Так шкала выглядит в деле — интерфейс, проза и метка на одном экране:
Заголовок раздела
Интерфейс по умолчанию: подписи, кнопки, ячейки.
Тело ответа агента набирается .inst-prose: 16/1.65,
пропорциональные цифры и строка, ограниченная 68 знаками.
Разметка
<div class="inst-stack">
<h2>Заголовок раздела</h2>
<p>Интерфейс по умолчанию: подписи, кнопки, ячейки.</p>
<div class="inst-prose">
<p>Тело ответа агента набирается <code>.inst-prose</code>: 16/1.65,
пропорциональные цифры и строка, ограниченная 68 знаками.</p>
</div>
<span class="inst-badge" data-tone="ok"><span class="inst-dot"></span>готово</span>
</div>
Контракт
Кегль берётся из шкалы, начертание — из трёх. Заголовок не имеет права кричать громче данных, которые он подписывает.
.my-label {
font-size: var(--text-xs);
color: var(--text-muted);
line-height: var(--leading-ui);
}
| Что | Обязательно | Почему |
|---|---|---|
Кегль из --text-* |
да | Семь ступеней. Восьмая, выдуманная на месте, ломает лестницу заголовков |
| Интерлиньяж по работе | да | --leading-ui для интерфейса, --leading-prose для текста, который читают абзацами |
| Три начертания | да | normal, medium, mono. Вес 700 в библиотеке запрещён |
| Никакого капса с трекингом | да | Он кричит громче числа, которое подписывает. text-transform не встречается в библиотеке ни разу |
Доступность
| Масштабирование | Кегли в rem, поэтому настройка «размер шрифта по умолчанию» в браузере работает. Высоты контролов — min-block-size, текст не упирается в потолок |
| Пол кегля | 11px. Ниже в библиотеке размера нет и не заводится |
| Порог чтения | Любой читаемый текст стоит на токене с порогом 4.5:1. --text-faint (3:1) для текста не применяется — см. цвет |
| Мера строки | Проза ограничена 68 знаками: длинная строка теряет начало следующей |
| Заголовки | Уровни h1–h6 — настоящие. Кегль не заменяет структуру: скринридер читает уровень, а не размер |
| Обрезка | .inst-u-truncate прячет текст визуально, но не от скринридера. Полный текст нужен в title — так делает инспектор |
Шкала
Семь ступеней, не больше. Каждая лишняя — это решение, которое кто-то примет неправильно.
| Токен | Значение | При корне 16px | Отношение | Для чего |
|---|---|---|---|---|
--text-2xs |
0.6875rem |
11px | — | Бейджи, подписи осей. Минимальный кегль, ниже не опускаемся |
--text-xs |
0.78125rem |
12.5px | 1.136 | Метаданные, время, подсказки |
--text-sm |
0.875rem |
14px | 1.12 | База интерфейса: подписи, кнопки, ячейки |
--text-md |
1rem |
16px | 1.143 | Проза, тела сообщений, имя панели |
--text-lg |
1.125rem |
18px | 1.125 | Заголовок блока |
--text-xl |
1.375rem |
22px | 1.222 | Заголовок раздела |
--text-2xl |
1.75rem |
28px | 1.273 | Только числа-герои |
Ступень существует, только если её видно. В рабочей части шкалы отношения держатся в полосе 1.12–1.143. Это порог: ниже него глаз перестаёт различать кегли, и соседние ступени начинают работать как одна — иерархию приходится целиком перекладывать на цвет и вес, а их разрядов мало. Дисплейные ступени расходятся шире, 1.222 и 1.273, и тоже намеренно: заголовок отличается от заголовка на бо́льшую долю, чем подпись от подписи.
Шкала считается снизу вверх, от пола в 11px, а не от корня документа. То, что
--text-md совпал с 16px, — удобное совпадение, а не привязка.
Начертаний по-прежнему два, и 700 запрещён. Значит, крупную иерархию несёт только размер — тем более он обязан быть различимым.
Кегль — единственный ярус в rem. Интерфейс обязан уважать увеличенный
размер шрифта по умолчанию в браузере: в админке сидят весь день, и это первая
настройка, которую делает человек с ослабленным зрением. Масштабирование
страницы работает и с px, но настройку «размер шрифта по умолчанию» px
игнорирует молча.
Всё остальное остаётся в px, потому что это геометрия, а не текст:
хайрлайн, точка состояния, жёлоб, радиус. Следствие, которое нельзя
игнорировать: если текст растёт, а высоты контролов заданы в px, текст
однажды упрётся в потолок — поэтому высоты применяются как min-block-size,
а не block-size.
Заголовки
Умолчания элементов, класса не требуют. Все несут --weight-medium,
--leading-tight, --tracking-tight и text-wrap: balance.
| Элемент | Кегль |
|---|---|
<h1> |
--text-2xl |
<h2> |
--text-xl |
<h3> |
--text-lg |
<h4> <h5> <h6> |
--text-md |
Заголовок раздела
Внутри .inst-prose у заголовка свой ритм: воздуха сверху
вдвое больше, чем снизу. Заголовок принадлежит тому, что под ним, и
разделять текст поровну ему нельзя.
Подзаголовок
Без этого правила заголовок висит посередине между своим абзацем и чужим — и не отделяет ни одного от другого.
Разметка
<div class="inst-prose">
<h2>Заголовок раздела</h2>
<p>Внутри <code>.inst-prose</code> у заголовка свой ритм: воздуха сверху
вдвое больше, чем снизу. Заголовок принадлежит тому, что под ним, и
разделять текст поровну ему нельзя.</p>
<h3>Подзаголовок</h3>
<p>Без этого правила заголовок висит посередине между своим абзацем и
чужим — и не отделяет ни одного от другого.</p>
</div>
ЗАГОЛОВКИ КАПСОМ и Title Case запрещены: только обычное предложение.
Варианты
Два. 700 в шрифте есть и всё равно запрещён: на макете из хайрлайнов и воздуха он читается как крик.
Медиум — 600, а не 500, и это признание факта. У Segoe UI настоящего 500 нет, и системный стек снапает его к Semibold: замер по пикселям показал, что 500 и 600 совпадают строка в строку. На macOS SF Pro даёт настоящий Medium — то есть рисунок различался на двух платформах, и выбирал это шрифт, а не автор.
| Токен | Значение | Где |
|---|---|---|
--weight-normal |
400 | Всё по умолчанию |
--weight-medium |
600 | Заголовки, подписи кнопок, шапка таблицы, бейдж |
Разметка
<div class="inst-stack inst-stack--tight">
<span>400 — обычный текст, ячейки, подписи полей</span>
<span style="font-weight:var(--weight-medium)">600 — заголовок, кнопка, шапка таблицы</span>
<span style="font-weight:700">700 — в библиотеке запрещено: на плоском макете читается как крик</span>
</div>
Интерлиньяж
| Токен | Значение | Для чего |
|---|---|---|
--leading-tight |
1.25 | Заголовки, однострочный интерфейс |
--leading-ui |
1.4 | База документа: переносящиеся подписи, ячейки таблиц |
--leading-prose |
1.65 | Проза |
Трекинг
Оптическая коррекция, не стиль: крупный кегль по умолчанию выглядит разреженным.
| Токен | Значение | Где |
|---|---|---|
--tracking-tight |
-0.01em |
Заголовки |
--tracking-normal |
0 |
Всё остальное |
Мелкий кегль не разрежается никогда: разрядка на 11px — это не акцент, а потеря слова.
Семейства
| Токен | Стек |
|---|---|
--font-sans |
ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial |
--font-mono |
ui-monospace, Cascadia Code, JetBrains Mono, SF Mono, Consolas, Liberation Mono |
Шрифт системный: веб-шрифт добавил бы сетевой запрос и вспышку подмены ради эффекта, который в плоском инструментальном интерфейсе не читается.
Моноширинный приходит вместе с оптической поправкой 0.925em — моно
воспринимается крупнее санса на том же кегле. Поправка объявлена один раз
на code, kbd, samp, pre и .inst-u-mono: когда у утилиты было своё
правило, <code class="inst-u-mono"> получал 0.925 × 0.925 и уезжал под пол
в 11px. Внутри блока кода поправка уже применена контейнером и второй раз не
накладывается.
Табличные цифры
font-variant-numeric: tabular-nums стоит на body, то есть по умолчанию
на всём. В инструментальном интерфейсе почти любое число либо стоит в колонке,
либо обновляется на месте, и пропорциональные цифры дёргают оба случая.
| Что | Как отключить |
|---|---|
| Абзац прозы | .inst-prose — уже отключает |
| Отдельный кусок текста | .inst-u-prose-nums |
Обратного переключателя нет: табличные и так везде.
Композиции
Опт-ин, а не умолчание. Базой должен быть частый случай, иначе каждый компонент
вынужден переобъявлять размер, а любой div приложения без класса наследует не
тот кегль.
<div class="inst-prose">
<p>Тело ответа агента.</p>
<p>Второй абзац отступает от первого сам.</p>
</div>
Что делает .inst-prose |
Значение |
|---|---|
| Кегль и интерлиньяж | --text-md / --leading-prose |
| Цифры | Пропорциональные |
| Ширина строки | 68ch — мера читаемости |
| Перенос | text-wrap: pretty у абзацев |
| Ритм | Зазор ставится между соседями: > * + * получает --space-5 |
| Списки | Отступ --space-7, между пунктами --space-2 |
Отдельного компонента для стриминга нет и не нужно: тело — это .inst-prose,
а признак того, что текст ещё поступает, — .inst-caret.
Клавиши
Инструмент живёт на клавиатуре, поэтому клавиша — элемент интерфейса, а не
безымянный моноширинный кусок. <kbd> оформлен в базе и класса не требует.
Разметка
<span><kbd>Ctrl</kbd> <kbd>K</kbd> — поиск по прогонам</span>
<span><kbd>Esc</kbd> — остановить прогон</span>
Правила
Что брать, а что нет
| Используйте | Возьмите другое |
|---|---|
--text-sm — умолчание интерфейса. Ничего не пишете, всё уже так |
Абзацы, которые читают подряд — класс .inst-prose, он же тело ответа агента |
--text-2xl — число-герой, одно на блок |
Заголовок экрана — --text-xl через <h2>. Крупный кегль у подписи кричит громче числа, которое он подписывает |
--text-2xs — бейдж, подпись оси |
Метаданные и время — --text-xs: 11px это пол, а не рабочий кегль. Пример — строка лога |
--weight-medium для акцента |
700 — его в библиотеке нет. Разделяйте кеглем и цветом, как в метрике |
| Моноширинный для идентификаторов и путей | Моноширинный для прозы — обычный текст остаётся сансом, см. блок кода |
API
| Имя | Что делает |
|---|---|
| класс | |
inst-prose | Абзацный режим: 16/1.65, пропорциональные цифры, 68ch |
inst-u-mono | Моноширинный кусок в потоке |
inst-u-prose-nums | Пропорциональные цифры точечно |
inst-u-dim | Приглушение до --text-secondary |
inst-u-truncate | Обрезка одной строки многоточием |
| Имя | Значение | Откуда |
|---|---|---|
| токен | ||
--font-sans | ui-sans-serif, system-ui, -apple-system, " , Roboto, " , Arial, sans-serif | одно на все ячейки |
--font-mono | ui-monospace, " , " , " , Consolas, " , monospace | одно на все ячейки |
--text-2xs/xs/sm/md/lg/xl/2xl | — | одно на все ячейки |
--weight-normal | 400 | одно на все ячейки |
--weight-medium | 600 | одно на все ячейки |
--leading-tight/ui/prose | — | одно на все ячейки |
--tracking-tight | -0.01em | одно на все ячейки |
--tracking-normal | 0 | одно на все ячейки |
Почему кит устроен именно так — конституция · Открытый код под MIT, github.com/keshon/instrument