Действия
Кнопка
Действие, которое пользователь запускает сам. По умолчанию кнопка тихая:
экран, где всё 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--ghost" type="button">Подробнее</button>
<button class="inst-btn inst-btn--danger" type="button">Удалить</button>
Использование
Минимальная кнопка — один класс и обязательный type.
<button class="inst-btn" type="button">Отмена</button>
type обязателен. Кнопка без него внутри формы имеет type="submit" по
умолчанию и отправляет форму — включая ту, которая всего лишь показывала
подсказку.
Кнопка бывает ссылкой. Переход по адресу обязан быть <a> — иначе ломаются
средний клик, «открыть в новой вкладке» и копирование адреса, — и класс на
ссылке даёт ровно ту же кнопку, без подчёркивания.
Разметка
<a class="inst-btn" href="#кнопка">Открыть отчёт</a>
<a class="inst-btn inst-btn--primary" href="#кнопка">Перейти к прогону</a>
<button class="inst-btn" type="button">Запустить</button>
Когда использовать
| Используйте | Возьмите другое |
|---|---|
| Действие, меняющее состояние: запустить, сохранить, удалить | Переход по адресу — это <a>. Кнопка, ведущая на другую страницу, ломает средний клик, «открыть в новой вкладке» и копирование ссылки |
Ровно одно primary на экран — главное действие |
Выбор одного из равных вариантов — сегментированный контрол, а не три кнопки |
| Несколько связанных действий вплотную — группа | Переключение вкл/выкл — свитч или чекбокс: у кнопки нет состояния «нажата» |
Опасное действие — --danger |
Второстепенная ссылка в тексте — обычная ссылка в потоке |
Варианты
Вариант — это две строки: он меняет только внутренние переменные, а не переписывает правило.
Разметка
<button class="inst-btn" type="button">По умолчанию</button>
<button class="inst-btn inst-btn--primary" type="button">Primary</button>
<button class="inst-btn inst-btn--ghost" type="button">Ghost</button>
<button class="inst-btn inst-btn--danger" type="button">Danger</button>
| Вариант | Когда |
|---|---|
| без модификатора | Умолчание. Рамка --border-control, прозрачная заливка |
inst-btn--primary |
Главное действие экрана. Одно на экран |
inst-btn--ghost |
Третьестепенное: рамка снята, остался только текст |
inst-btn--danger |
Необратимое или разрушительное действие |
Размеры
Разметка
<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--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 |
Состояния
Разметка
<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>
Четыре решения, каждое из которых легко сделать наоборот и получить дефект:
- Подпись не убирается из потока, а гасится цветом. Ширина кнопки остаётся прежней. Иначе полоса действий дёргается в тот момент, когда пользователь смотрит на курсор.
- Подпись остаётся в разметке — её читает скринридер, и запасной
aria-labelне нужен. - Кнопка сохраняет фокус.
disabledвыбросил бы её из порядка обхода прямо под руками у того, кто пришёл с клавиатуры. - Кит снимает только мышь (
pointer-events). Защита от повторного нажатия — в обработчике: это поведение, а не оформление.
С иконкой
Разметка
<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>
Иконочная кнопка квадратная. Прямоугольник 32×28 — самый частый признак самодельного кита. Ширина берётся из той же высоты контрола, поэтому квадрат сохраняется во всех трёх размерах и во всех плотностях.
Кнопка без текста обязана нести 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>
Несколько разных действий, поставленных вплотную. Не путать с сегментированным контролом: там один выбор среди равных, и у него всегда ровно одно выбранное значение.
Шов группы — снятая рамка у соседа, а не отрицательное поле в -0.5px:
браузер никогда не рисует рамку тоньше физического пикселя, поэтому сдвиг на
полпикселя оставлял видимое утолщение на каждом стыке.
Сценарии
Полоса действий формы
Главное действие последнее, отмена перед ним, обе прижаты к дальнему краю.
Разметка
<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">
<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>
Правила
Так Одно primary на экран
Главное действие ровно одно. Второе primary не добавляет важности первому —
оно её отнимает.
Не так Кнопка вместо ссылки
Переход по адресу — это <a>. Кнопка ломает средний клик, «открыть в новой
вкладке» и копирование адреса.
Так Занятость через aria-busy
Кнопка остаётся в порядке обхода, подпись читается скринридером, ширина не меняется.
Не так Занятость через disabled
disabled выбрасывает кнопку из обхода с клавиатуры прямо в момент нажатия и
не сообщает, что действие пошло.
Так Иконочная кнопка с aria-label
inst-btn--icon без доступного имени — кнопка, которую нечем назвать.
Не так Цвет как единственный признак
danger отличается не только тоном: подпись называет действие словом
«Удалить», а не «ОК».
Доступность
| Клавиатура | Tab — фокус, Enter и Space — нажатие. Всё от нативного <button>, кит ничего не перехватывает |
| Фокус | :focus-visible — кольцо появляется от клавиатуры и не появляется от мыши. В группе фокусная кнопка поднимается по z-index, чтобы кольцо не срезал сосед |
| Контраст | Подпись каждого варианта проверена на 4.5:1 в шести темах. Заливка primary ограничена сверху по светлоте: светлее 0.56 белая подпись перестаёт держать порог |
| Занятость | aria-busy озвучивается, подпись остаётся |
| Недоступность | disabled — нативная. Прозрачность не роняет подпись ниже порога, потому что читается вместе с фоном |
| Цель нажатия | md — 32px, sm — 26px. В плотности compact sm даёт 22px, что ниже 24px по WCAG 2.5.8: применяйте compact к плотным табличным видам, а не к основной навигации |
| Уменьшенное движение | Кольцо занятости замедляется, но не останавливается: остановленный индикатор сообщает «ничего не происходит», когда происходит |
Настройка
Свой вариант — две строки. Переопределяются переменные, а не правило.
.my-btn--accent {
--btn-fg: var(--accent-on);
--btn-bg: var(--accent-solid);
}
Два ограничения, которые проще нарушить, чем заметить:
- Наведение меняет только
--btn-bg. Не переопределяйтеbackgroundнапрямую — переменная перестанет читаться, и вариант потеряет ховер. - Заливка под белой подписью не может быть светлее
0.56по OKLCH. Выше этого порога белый текст перестаёт держать 4.5:1. По той же причине--btn-bg-hoverуходит от цвета подписи. Зеркальное «в тёмной теме светлее» роняет контраст подписи до 2.61.
API
| Имя | Значение | Что делает |
|---|---|---|
| класс | ||
inst-btn | — | Базовый. На <button> или <a>. Обязателен всегда |
inst-btn-group | — | Контейнер группы: снимает рамку у соседа и скругляет только торцы |
| модификатор | ||
inst-btn--primary | — | Сплошная заливка акцентом. Одно главное действие на экран |
inst-btn--ghost | — | Без рамки. Для второстепенных действий в плотном ряду |
inst-btn--danger | — | Разрушительное действие: тон ошибки в подписи и рамке |
inst-btn--sm | 26px | Высота из --control-h-sm, кегль --text-xs |
inst-btn--lg | 38px | Высота из --control-h-lg, кегль --text-md |
inst-btn--icon | — | Квадратная, только иконка. Требует aria-label |
| атрибут | ||
type | button · submit | Обязателен. Без него кнопка внутри формы её отправит |
aria-busy | true | Действие пошло: подпись гаснет, на её месте крутится кольцо. Ширина не меняется |
aria-label | — | Обязателен у inst-btn--icon: кнопка без текста не имеет доступного имени |
disabled | — | Недоступно. Для занятости не применяется — выбрасывает кнопку из обхода с клавиатуры |
| переменная | ||
--btn-fg | --text-primary | Цвет подписи |
--btn-bg | transparent | Заливка. Наведение меняет только её |
--btn-bg-hover | --surface-hover | Заливка под курсором |
--btn-bg-active | --surface-active | Заливка при нажатии |
--btn-border | --border-control | Рамка |
| токен | ||
--control-h-md | 32px | Высота. Перенастраивается плотностью |
--control-pad-md | 12px | Горизонтальный отступ |
--radius-md | 7px | Скругление отдельно стоящего контрола |
--text-sm | 13px | Кегль подписи |
--weight-medium | 500 | Начертание подписи |
--size-spinner | 13px | Кольцо занятости |
--dur-1 | 80ms | Длительность перехода заливки |
Почему кит устроен именно так — конституция · Открытый код под MIT, github.com/keshon/instrument