Действия
Кнопка
Действие, которое пользователь запускает сам. По умолчанию кнопка тихая:
экран, где всё primary, не имеет primary вовсе.
Разметка
<button class="inst-btn inst-btn--primary" type="button">Запустить</button>
<button class="inst-btn" type="button">Отмена</button>
<button class="inst-btn inst-btn--soft" type="button">Фильтр</button>
<button class="inst-btn inst-btn--ghost" type="button">Подробнее</button>
<button class="inst-btn inst-btn--danger" type="button">Удалить</button>
Контракт
| Что | Обязательно | Почему |
|---|---|---|
.inst-btn |
да | Базовый класс. На <button> или на <a> |
type у <button> |
да | Кнопка без него внутри формы имеет type="submit" по умолчанию и отправляет форму — включая ту, которая всего лишь показывала подсказку |
<a> для перехода по адресу |
да | Кнопка, ведущая на другую страницу, ломает средний клик, «открыть в новой вкладке» и копирование адреса. Класс на ссылке даёт ровно ту же кнопку |
aria-busy="true" на время работы |
да, если кнопка ждёт | Озвучивается, подпись при этом остаётся на месте |
disabled |
нативный | Прозрачность не роняет подпись ниже порога: она читается вместе с фоном |
Бегущий tabindex |
нет | Tab, Enter и Space целиком от нативной <button>; instrument.js здесь не перехватывает ничего |
aria-label на inst-btn--icon |
да | Кнопка без доступного имени — кнопка, которую нечем назвать |
<button>, а не <input> ради вида |
да | Подменять кнопку полем — ложь скринридеру про то, что здесь можно печатать, и сломанная клавиатура: поле не открывается пробелом |
:focus-visible — кольцо появляется от клавиатуры и не появляется от мыши.
В группе фокусная кнопка поднимается по z-index, чтобы кольцо не срезал
сосед.
Подпись каждого варианта проверена на 4.5:1 в пяти темах, а заливка
primary ограничена сверху по светлоте, и предел хью-зависимый: 0.545 у
петроли, стоящей умолчанием. Цель нажатия — 32px у md и 24px у sm; в
плотности compact sm даёт 22px, что ниже 24px по WCAG 2.5.8, и
потому compact применяют к плотным табличным видам, а не к основной
навигации. Кольцо занятости при prefers-reduced-motion замедляется, но
не останавливается: остановленный индикатор сообщает «ничего не
происходит», когда происходит.
Варианты
Вариант — это две строки: он меняет только внутренние переменные, а не переписывает правило.
Разметка
<button class="inst-btn" type="button">По умолчанию</button>
<button class="inst-btn inst-btn--primary" type="button">Primary</button>
<button class="inst-btn inst-btn--soft" type="button">Soft</button>
<button class="inst-btn inst-btn--outline" type="button">Outline</button>
<button class="inst-btn inst-btn--ghost" type="button">Ghost</button>
<button class="inst-btn inst-btn--danger" type="button">Danger</button>
| Вариант | Когда |
|---|---|
| без модификатора | Умолчание — утопленная: провал по светлоте, ни рамки, ни тени. Глубина считается от подложки, поэтому шаг одинаков на странице, в панели и в модалке |
inst-btn--primary |
Главное действие экрана. Одно на экран |
inst-btn--soft |
Контрол внутри группы: приглушённая заливка, рамки нет |
inst-btn--outline |
Обводка: тихая рамка, заливки нет. Действие, которое обязано выглядеть нажимаемым, не претендуя на внимание |
inst-btn--ghost |
Третьестепенное: ни рамки, ни заливки — только текст |
inst-btn--danger |
Необратимое или разрушительное действие. Тон, а не вес: ставится поверх любого из четырёх и красит подпись, не добавляя рамки |
data-tone |
Тон подписи из общего словаря: running — акцентное действие без хрома, ok, warn, error, neutral. Ставится поверх любого веса, кроме --primary |
Лестница весов
Вес отвечает на «насколько громко кнопка просит нажать себя», и ответ измерим: присутствие — это площадь, взвешенная шагом светлоты к земле.
| Вес | светлая | тёмная |
|---|---|---|
--ghost |
0 | 0 |
--outline |
0.007 | 0.007 |
--soft |
0.017 | 0.022 |
| умолчание | 0.046 | 0.048 |
--primary |
0.47 | 0.17 |
Монотонна во всех пяти темах, соседние ступени расходятся не меньше чем на
0.022 — это проверяет cmd/contrast парой «лестница: soft против default».
Рамка у outline декоративная, а не несущая.
Тон
Тон — общий словарь кита, тот же, что у бейджа, точки, меры и баннера. Кнопка
читает из него --tone-ink для подписи и --tone-bg для наведения.
Разметка
<button class="inst-btn inst-btn--ghost" type="button" data-tone="running">Добавить контекст</button>
<button class="inst-btn" type="button" data-tone="ok">Принять</button>
<button class="inst-btn" type="button" data-tone="error">Удалить</button>
<button class="inst-btn inst-btn--danger" type="button">Удалить</button>
Последние две строки дают одно и то же: inst-btn--danger — это тот же тон
error, записанный классом. Класс остался, потому что разрушительное действие
встречается часто и заслуживает короткого написания.
Призрачная с тоном running — то самое действие-ссылка: подпись акцентом, ни
рамки, ни заливки. Ею набирают «добавить контекст», «добавить строку», «создать
первый объект» — то, что приглашает нажать, но не спорит с главной кнопкой
экрана.
| Что | Почему |
|---|---|
| Тон ставится на саму кнопку | --tone-ink наследуется, и правило «просто читать переменную» покрасило бы кнопку внутри аварийного баннера само собой. Тон объявляется, а не подхватывается |
--primary тон не принимает |
Подпись там стоит на заливке акцентом и держит 4.5:1 только белым. Красный или зелёный на этой заливке даёт 1.5–2.0, поэтому сочетание обезврежено явно |
| Тон не заменяет вес | Вес отвечает, насколько громко кнопка просит нажать себя; тон — что за действие. Вопросы разные, поэтому и оси разные |
Переключатель значения
Есть контрол, у которого главное — значение, а подпись лишь говорит, чего это значение: текущий репозиторий, ветка, окружение, учётная запись. «master» сам по себе не значит ничего, и убрать «Current branch» нельзя.
Разметка
<button class="inst-btn inst-btn--stacked" type="button" style="inline-size:15rem">
<svg class="inst-icon" aria-hidden="true"><use href="#i-book"/></svg>
<span class="inst-btn-stack">
<span class="inst-btn-label">Текущий репозиторий</span>
<span class="inst-btn-value">instrument-ui-kit</span>
</span>
<svg class="inst-icon inst-icon--sm" aria-hidden="true"><use href="#i-chevron"/></svg>
</button>
Колонкой становится текст, поэтому .inst-btn-stack обязателен: у такого
контрола есть ведущая иконка и шеврон, и колонка из всей кнопки положила бы их
сверху и снизу. Две строки — свойство этого варианта, а не разрешение переносить
любую кнопку.
Кнопка в одежде поля
Разметка
<button class="inst-btn inst-btn--field" type="button" style="inline-size:18rem">
<svg class="inst-icon" aria-hidden="true"><use href="#i-list"/></svg>
Выберите файл или команду
</button>
Открывает палитру или список, но остаётся кнопкой. Вид берётся у вреза, а поведение — кнопочное: рамка здесь несущая, потому что она и есть контрол, внутри одна подсказка.
Ссылкой
Класс на <a> даёт ровно ту же кнопку, без подчёркивания.
Разметка
<a class="inst-btn" href="#кнопка">Открыть отчёт</a>
<a class="inst-btn inst-btn--primary" href="#кнопка">Перейти к прогону</a>
<button class="inst-btn" type="button">Запустить</button>
Размеры
Разметка
<button class="inst-btn inst-btn--xs" type="button">xs</button>
<button class="inst-btn inst-btn--sm" type="button">sm</button>
<button class="inst-btn" type="button">md</button>
<button class="inst-btn inst-btn--lg" type="button">lg</button>
Высота приходит из яруса ролей (--control-h-*), поэтому кнопка и стоящее
рядом поле того же размера физически не могут разойтись.
Плотность контейнера перенастраивает все три размера разом.
| Размер | Высота | Отступ | Кегль |
|---|---|---|---|
inst-btn--xs |
--control-h-xs |
--space-3 |
--text-2xs |
inst-btn--sm |
--control-h-sm |
--control-pad-sm |
--text-xs |
| по умолчанию | --control-h-md |
--control-pad-md |
--text-sm |
inst-btn--lg |
--control-h-lg |
--control-pad-lg |
--text-md |
xs стоит особняком: бокс у неё от бейджа, а не из яруса
контролов, — ниже sm соседом в строке оказывается чип, а не кнопка. Она для
контрола, который принадлежит чему-то другому: значки под ответом, настройки
сессии под полем ввода. Цель нажатия остаётся
--tap-min через псевдоэлемент — рисунок уменьшается, цель нет. Отсюда
требование к строке: зазор не меньше --space-3. При compact бокс равен
18px, и на меньшем зазоре увеличенные цели перекрываются. Главная кнопка,
стоящая сама по себе, остаётся sm.
Состояния
Разметка
<button class="inst-btn" type="button">Обычная</button>
<button class="inst-btn" type="button" disabled>Недоступна</button>
<button class="inst-btn inst-btn--primary" type="button" aria-busy="true">Запустить</button>
| Состояние | Как ставится | Что происходит |
|---|---|---|
| наведение | :hover:not(:disabled) |
Меняется только --btn-bg |
| нажатие | :active:not(:disabled) |
--btn-bg + scale(0.985) — нажатие надо почувствовать, а не разглядывать |
| фокус | :focus-visible |
Кольцо с отступом 1px, без собственного радиуса |
| недоступна | disabled |
Прозрачность 0.5, а не серая заливка: кнопка сохраняет идентичность, и видно, какое именно действие недоступно |
| занята | aria-busy="true" |
Подпись гаснет, на её месте кольцо. См. ниже |
Занятость
Разметка
<button class="inst-btn inst-btn--primary" type="button" aria-busy="true">Запустить</button>
<button class="inst-btn" type="button" aria-busy="true">Отмена</button>
<button class="inst-btn inst-btn--icon" type="button" aria-busy="true" aria-label="Обновить">
<svg class="inst-icon" aria-hidden="true"><use href="#i-refresh"/></svg>
</button>
Подпись не убирается из потока, а гасится цветом: ширина держится, и скринридер
её по-прежнему читает. Фокус кнопка сохраняет — disabled выбросил бы её из
обхода прямо под руками у того, кто пришёл с клавиатуры, — а снимается только
мышь: защита от второго нажатия принадлежит обработчику.
С иконкой
Разметка
<button class="inst-btn" type="button">
<svg class="inst-icon" aria-hidden="true"><use href="#i-refresh"/></svg>
Обновить
</button>
<button class="inst-btn inst-btn--icon" type="button" aria-label="Обновить">
<svg class="inst-icon" aria-hidden="true"><use href="#i-refresh"/></svg>
</button>
Иконочная кнопка квадратная: ширина берётся из той же высоты контрола, поэтому квадрат держится во всех трёх размерах и при любой плотности.
Кнопка без текста обязана нести aria-label. Иконка при этом
aria-hidden="true" — иначе скринридер прочитает её дважды или не прочитает
вовсе.
Композиции
Группа
Разметка
<div class="inst-btn-group">
<button class="inst-btn inst-btn--sm" type="button">Слева</button>
<button class="inst-btn inst-btn--sm" type="button">Центр</button>
<button class="inst-btn inst-btn--sm" type="button">Справа</button>
</div>
Несколько разных действий, поставленных вплотную, — чем группа отличается от сегментации и от чипа, сказано в индексе раздела.
Сценарии
Полоса действий формы
Главное действие стоит у внешнего края полосы. Здесь полоса прижата к концу строки, поэтому главное идёт последним, а отмена перед ним; при выравнивании к началу порядок обратный — см. форму.
Разметка
<form class="inst-panel">
<div class="inst-panel-body">
<div class="inst-field">
<label class="inst-label" for="p-name">Имя прогона</label>
<input class="inst-input" id="p-name" type="text" value="nightly-4127">
</div>
<div class="inst-form-actions inst-form-actions--end">
<button class="inst-btn" type="button">Отмена</button>
<button class="inst-btn inst-btn--primary" type="submit">Сохранить</button>
</div>
</div>
</form>
Действия строки таблицы
Плотный ряд — ghost и sm: рамка у каждой кнопки в таблице превратила бы
колонку в решётку. Разрушительное действие рамку сохраняет — это единственное
в ряду, которое обязано быть заметно.
Разметка
<div class="inst-cluster">
<button class="inst-btn inst-btn--sm inst-btn--ghost" type="button">Открыть</button>
<button class="inst-btn inst-btn--sm inst-btn--ghost" type="button">Повторить</button>
<button class="inst-btn inst-btn--sm inst-btn--danger" type="button">Удалить</button>
</div>
Настройка
Свой вариант — две строки. Переопределяются переменные, а не правило.
.my-btn--accent {
--btn-fg: var(--accent-on);
--btn-bg: var(--accent-solid);
}
Два ограничения, которые проще нарушить, чем заметить:
- Наведение меняет только
--btn-bg. Не переопределяйтеbackgroundнапрямую — переменная перестанет читаться, и вариант потеряет ховер. - Заливка под белой подписью не может быть светлее предела для своего
хью — 0.545 на 215°, 0.568 на 250°, 0.576 на 275°, 0.580 на 45°.
Выше этого порога белый текст перестаёт держать 4.5:1. По той же причине
--btn-bg-hoverуходит от цвета подписи. Зеркальное «в тёмной теме светлее» роняет контраст подписи до 2.61.
API
| Имя | Что делает |
|---|---|
| класс | |
inst-btn | Базовый. На <button> или <a>. Обязателен всегда |
inst-btn-group | Контейнер группы: снимает рамку у соседа и скругляет только торцы |
inst-btn-stack | Столбик текста внутри --stacked. Обязателен: колонкой становится текст, а не кнопка, иначе значок уедет над подписью |
inst-btn-label | Подпись внутри .inst-btn-stack. Тише и мельче значения |
inst-btn-value | Значение внутри .inst-btn-stack. Длинное обрезается: это идентификатор, переносить его нельзя |
inst-btn--outline | Обводка: тихая рамка, заливки нет. Ступень между призрачной и мягкой |
| модификатор | |
inst-btn--primary | Сплошная заливка акцентом. Одно главное действие на экран |
inst-btn--soft | Приглушённая заливка, рамки нет. Контрол внутри группы: тулбар, ряд значков, чип выбора |
inst-btn--stacked | Две строки: тихая подпись сверху, значение снизу. Для контрола, у которого главное — значение (репозиторий, ветка, окружение) |
inst-btn--field | Вид вреза, поведение кнопки. Открывает палитру или список; подменять настоящим <input> нельзя — это ложь скринридеру |
inst-btn--ghost | Без рамки и без заливки. Для второстепенных действий в плотном ряду |
inst-btn--danger | Разрушительное действие: тон ошибки в подписи. Это ТОН поверх веса, а не пятый вес — сочетается с любым |
inst-btn--xs | Бокс бейджа: --control-h-xs, --space-3, --radius-xs. Для контрола, принадлежащего чему-то другому. Цель нажатия остаётся --tap-min через ::before |
inst-btn--sm | Высота из --control-h-sm, кегль --text-xs |
inst-btn--lg | Высота из --control-h-lg, кегль --text-md |
inst-btn--icon | Квадратная, только иконка. Требует aria-label |
| Имя | Значение | Что делает |
|---|---|---|
| атрибут | ||
type | button · submit | Обязателен. Без него кнопка внутри формы её отправит |
aria-busy | true | Действие пошло: подпись гаснет, на её месте крутится кольцо. Ширина не меняется |
aria-label | — | Обязателен у inst-btn--icon: кнопка без текста не имеет доступного имени |
disabled | — | Недоступно. Для занятости не применяется — выбрасывает кнопку из обхода с клавиатуры |
data-tone | running · ok · warn · error · neutral | Тон подписи из общего словаря кита. Ставится на саму кнопку |
| Имя | Значение | Откуда |
|---|---|---|
| переменная | ||
--btn-fgЦвет подписи | --text-primary | задаёт приложение |
--btn-bgЗаливка. Наведение меняет только её | --surface-raised | задаёт приложение |
--btn-bg-hoverЗаливка под курсором | --surface-hover | задаёт приложение |
--btn-bg-activeЗаливка при нажатии | --surface-active | задаёт приложение |
--btn-borderРамка. Волосяная: опознаёт кнопку подпись, а не контур | --border | задаёт приложение |
--btn-shadowУ умолчания тени нет — глубину несёт провал. Ставится у --primary: там она вместе с кромкой даёт толщину сплошной заливке | none | задаёт приложение |
| токен | ||
--control-h-mdВысота. Перенастраивается плотностью | 32px | база +14: плотность · масштаб |
--control-pad-mdГоризонтальный отступ. Выводится из высоты через --control-ratio-md | 12px | одно на все ячейки |
--radius-mdСкругление отдельно стоящего контрола | 8px | база +4: масштаб |
--shadow-controlТень в один пиксель. Не «плавает сверху», а «имеет толщину» | 0 1px 2px -1px var(--shadow-color-far), 0 1px 1px -1px var(--shadow-color-near) | одно на все ячейки |
--gap-inlineПросвет между значком и подписью | 8px | база +14: плотность · масштаб |
--text-mutedЦвет значка: он тише подписи, которую сопровождает | light-dark(var(--n-8), var(--n-6)) | одно на все ячейки |
--text-smКегль подписи | 14px | база +4: масштаб |
--weight-mediumНачертание подписи | 600 | одно на все ячейки |
--size-spinnerКольцо занятости | 14px | база +14: плотность · масштаб |
--dur-1Длительность перехода заливки | 80ms | одно на все ячейки |
Почему кит устроен именно так — конституция · Открытый код под MIT, github.com/keshon/instrument