Действия
Сегментированный контрол
Один выбор среди равных вариантов. У него всегда ровно одно выбранное значение — и в этом всё отличие от группы кнопок, где выбранного нет и быть не может.
Разметка
<div class="inst-segmented" role="radiogroup" aria-label="Режим отображения">
<button type="button" role="radio" aria-checked="true">Список</button>
<button type="button" role="radio" aria-checked="false">Сетка</button>
<button type="button" role="radio" aria-checked="false">Таблица</button>
</div>
Использование
Кит рисует состояние, но не выдумывает его. Без ролей aria-checked
невалиден, а с клавиатуры до контрола не добраться.
<div class="inst-segmented" role="radiogroup" aria-label="Плотность">
<button type="button" role="radio" aria-checked="true" tabindex="0">Плотно</button>
<button type="button" role="radio" aria-checked="false" tabindex="-1">Обычно</button>
</div>
| Что | Почему обязательно |
|---|---|
role="radiogroup" на контейнере |
Группа — один контрол с одним значением |
aria-label на контейнере |
Иначе у контрола нет имени: подписи есть только у вариантов |
role="radio" на каждой кнопке |
role="tab" без панели — невалидная роль, и раньше стили висели именно на ней |
aria-checked на каждой |
Носитель состояния. Стиль выбранного висит на нём, а не на классе |
type="button" |
Иначе внутри формы кнопка её отправит |
Бегущий tabindex |
0 у выбранной, -1 у остальных: Tab входит в группу один раз, дальше стрелки |
Когда использовать
| Используйте | Возьмите другое |
|---|---|
| 2–4 равноправных варианта одного параметра: плотность, режим показа | Больше пяти вариантов — селект: сегментированный расползётся по ширине |
| Выбор виден целиком и переключается часто | Переключение вкл/выкл — свитч: у одного варианта нет пары |
| Варианты называются одним-двумя словами | Переход между разделами — вкладки: это роутинг, а не выбор значения |
| — | Разные действия вплотную — группа кнопок |
Варианты со своим адресом
Значение, у которого есть адрес, не перестаёт быть значением. Окно истории
переживает перезагрузку, ложится в закладку и передаётся другому человеку
целиком — ссылкой; выбором одного из равных оно от этого быть не перестало.
Носитель состояния тогда aria-current="page", а не aria-checked.
Разметка
<nav class="inst-segmented" aria-label="Окно истории">
<a href="?window=day" aria-current="page">24 часа</a>
<a href="?window=week">7 дней</a>
<a href="?window=month">30 дней</a>
</nav>
| Что | Почему |
|---|---|
nav + aria-label вместо role="radiogroup" |
Ссылки — не радиокнопки. Роль группы обещала бы стрелки и одно значение, а браузер тут ходит по ссылкам обычным Tab |
aria-current="page" |
Единственный носитель выбора. aria-checked на ссылке невалиден |
Без tabindex |
Бегущий фокус — механика радиогруппы. У ссылок его нет и не должно быть |
Отличать от вкладок просто: вкладки меняют раздел (другое содержимое страницы), сегментированный — параметр того же содержимого. Одиннадцать мониторов остаются теми же одиннадцатью, меняется только окно, за которое их показывают.
Состояния
Разметка
<div class="inst-segmented" role="radiogroup" aria-label="Выбрано первое">
<button type="button" role="radio" aria-checked="true">Выбрано</button>
<button type="button" role="radio" aria-checked="false">Обычное</button>
<button type="button" role="radio" aria-checked="false" disabled>Недоступно</button>
</div>
| Состояние | Как ставится | Что происходит |
|---|---|---|
| выбрано | aria-checked="true" — или aria-current="page" у ссылок |
Поднятая поверхность, рамка волоском, начертание medium |
| обычное | aria-checked="false" |
Подпись --text-secondary, фона нет |
| наведение | :hover |
Меняется только цвет подписи: поверхность занята выбранным |
| фокус | :focus-visible |
Кольцо внутри контейнера, без собственного радиуса |
| недоступно | disabled |
Прозрачность. Вариант остаётся видимым — важно, какой именно недоступен |
Выбранный несёт три признака: поверхность, начертание и рамку. Один тон различать не требуется.
JS
Подключите модуль один раз на страницу — инициализировать компоненты по отдельности не нужно, кит работает делегированием и видит узлы, пришедшие позже.
<script type="module" src="src/kit.js"></script>
Что делает кит
Примеры на этой странице живые: войдите в контрол Tab, дальше ← и →. Мышью — тоже.
Контрол объявлен как role="radiogroup", и кит выполняет обещание: ← и →
между вариантами, Home и End, один Tab на всю группу. Отметка
aria-checked следует за фокусом и переносится щелчком — сразу на всю группу,
потому что два отмеченных варианта это состояние, из которого разметка уже не
выйдет.
События
inst:select всплывает с выбранного варианта.
group.addEventListener('inst:select', (e) => {
document.documentElement.dataset.density = e.detail.value;
});
Опции
| Атрибут | Что делает |
|---|---|
data-value |
Значение в detail вместо подписи варианта |
aria-orientation="vertical" |
Стрелки по вертикали |
<div class="inst-segmented" role="radiogroup" aria-label="Плотность">
<button type="button" role="radio" aria-checked="true" data-value="sm" tabindex="0">Плотно</button>
<button type="button" role="radio" aria-checked="false" data-value="md" tabindex="-1">Обычно</button>
</div>
Композиции
В шапке экрана
Прогоны
Разметка
<div class="inst-page-header">
<div class="inst-page-header-main">
<h2 class="inst-page-title">Прогоны</h2>
</div>
<div class="inst-page-actions">
<div class="inst-segmented" role="radiogroup" aria-label="Период">
<button type="button" role="radio" aria-checked="false">День</button>
<button type="button" role="radio" aria-checked="true">Неделя</button>
<button type="button" role="radio" aria-checked="false">Месяц</button>
</div>
</div>
</div>
Правила
Так Два-четыре варианта
Выбор виден целиком, и переключение стоит одного нажатия. Это единственное преимущество перед селектом.
Не так Шесть вариантов в ряд
Контрол расползается по ширине и перестаёт помещаться в шапку. Шесть — это селект.
Так Ровно один aria-checked="true"
Носитель состояния — атрибут, а не класс. Ноль выбранных или два — невалидная группа радио.
Не так Вкладки под видом сегментированного
Переход между разделами — это роутинг: вкладки на ссылках, у которых работает средний клик.
Доступность
| Клавиатура | Tab — вход в группу и выход из неё. ← → ↑ ↓ — перебор вариантов, выбор следует за фокусом. Поведение — на приложении |
| Фокус | Бегущий tabindex обязателен: без него Tab пройдёт по всем вариантам подряд и группа перестанет быть одним контролом |
| Не только цвет | Выбранный несёт три признака: поверхность, начертание и кольцо рамки. Один тон различать не требуется |
| Контраст | Подпись невыбранного — --text-secondary, проверена на 4.5:1 в шести темах |
| Цель нажатия | Высота на --space-2 меньше --control-h-md. В плотности compact уходит ниже 24px по WCAG 2.5.8 — как и всё размера sm |
API
| Имя | Значение | Что делает |
|---|---|---|
| класс | ||
inst-segmented | — | Контейнер. Внутри — голые button или a, свой класс им не нужен |
| атрибут | ||
aria-checked | — | true · false на кнопках. Ровно один true в группе |
aria-current | page | Носитель выбора, если варианты — ссылки |
| токен | ||
--control-h-md | 32px | |
--control-pad-md | var(--space-5) | |
--radius-md | 7px | |
--radius-sm | 5px | |
--space-1 | 2px | |
--text-sm | 0.8125rem | |
--surface-sunken | light-dark(var(--n-2), var(--n-14)) | |
--surface-raised | light-dark(var(--n-0), var(--n-12)) | |
--border | light-dark(oklch(0 0 0 / 0.12), oklch(1 0 0 / 0.11)) | |
--hairline | 1px | |
Почему кит устроен именно так — конституция · Открытый код под MIT, github.com/keshon/instrument