Перейти к содержимому
Плотность
Тема

Действия

Сегментированный контрол

JS Выбор, стрелки и бегущий 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">Таблица</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-checkedtrue · false на кнопках. Ровно один true в группе
aria-currentpageНоситель выбора, если варианты — ссылки
токен
--control-h-md32px
--control-pad-mdvar(--space-5)
--radius-md7px
--radius-sm5px
--space-12px
--text-sm0.8125rem
--surface-sunkenlight-dark(var(--n-2), var(--n-14))
--surface-raisedlight-dark(var(--n-0), var(--n-12))
--borderlight-dark(oklch(0 0 0 / 0.12), oklch(1 0 0 / 0.11))
--hairline1px

Почему кит устроен именно так — конституция · Открытый код под MIT, github.com/keshon/instrument