Действия
Чип
Фильтр, который видно и который включают нажатием. Включённых бывает несколько сразу — и в этом отличие от сегментированного контрола, где выбранное значение всегда ровно одно.
Код
<div class="inst-chips" role="listbox" aria-multiselectable="true"
aria-orientation="horizontal" aria-label="Фильтр по состоянию">
<span class="inst-chip" role="option" aria-selected="true" tabindex="0">идёт</span>
<span class="inst-chip" role="option" aria-selected="true" tabindex="-1">упало</span>
<span class="inst-chip" role="option" aria-selected="false" tabindex="-1">в очереди</span>
<span class="inst-chip" role="option" aria-selected="false" tabindex="-1">готово</span>
<span class="inst-chip" role="option" aria-selected="false" aria-disabled="true" tabindex="-1">отменено</span>
</div>
chips.addEventListener('inst:select', (e) => {
const on = e.detail.selected ? 'включён' : 'выключен';
console.log(`фильтр ${e.detail.value} ${on}`);
});
Контракт
| Что | Обязательно | Почему |
|---|---|---|
role="listbox" на полосе |
да | Без него role="option" невалиден, а стрелки не заработают |
aria-label на полосе |
да | «Фильтр по состоянию»: иначе скринридер объявит список без имени |
aria-selected на каждом чипе |
да | Носитель выбора. false тоже пишется — отсутствие атрибута значит «выбор здесь не предусмотрен» |
tabindex |
да, начальный | Ноль у одного, -1 у остальных. Дальше его ведёт instrument.js. Без бегущего tabindex Tab пройдёт по каждому чипу и полоса перестанет быть одним контролом |
aria-orientation="horizontal" |
да | Полоса идёт вбок, а listbox по умолчанию вертикальный: без атрибута перебор повесился бы на ↑ ↓ при ряде, идущем вправо |
aria-multiselectable="true" |
нет | Ставится, когда включённых бывает несколько. Без него полоса ведёт себя как одиночный список: выбор идёт за фокусом |
Клавиатура: Tab — вход в полосу и выход из неё, ← → — перебор, пробел —
включить или выключить.
Выбранный чип несёт два признака, а не один цвет: цвет подписи и заливку.
Наведение занимает третий канал — рамку — и потому не спорит с выбором. Подпись
невыбранного --text-secondary, выбранного --accent-text, обе проверены на
4.5:1 в пяти темах и четырёх акцентах. Цель нажатия — высота --control-h-sm
с зазором полосы --space-2, проверено cmd/targets во всех пятнадцати
сочетаниях масштаба и плотности.
Событие inst:select приходит с detail.selected — включили или выключили.
Отменённое событие оставляет разметку нетронутой: приложение взяло состояние на
себя и поставит aria-selected само.
Состояния
Чип не имеет тона. Он называет фильтр, а не состояние: покрасив «упало» в красный, мы сказали бы, что сам фильтр аварийный. Цвет в чипе означает ровно одно — включён он или нет.
Три канала, и они не пересекаются, поэтому читаются вместе:
| Канал | Что кодирует |
|---|---|
| Цвет подписи | выбран — --accent-text, нет — --text-secondary |
| Цвет рамки | под курсором — --accent-border, иначе — --border |
| Заливка | есть, если выбран или под курсором |
Начертание при выборе не меняется намеренно: полоса переносится по ширине, и потолстевшая подпись переложила бы соседей на другую строку в момент нажатия.
JS
Модуль подключается один раз на страницу — инициализировать компоненты по отдельности не нужно.
Что делает instrument.js
Примеры на этой странице живые: войдите в полосу Tab, дальше ← и →,
включение — пробелом. Мышью тоже.
Полоса объявлена как role="listbox", и instrument.js выполняет её контракт:
← → между чипами, Home и End, один Tab на всю полосу.
Одиночный и множественный
Атрибут aria-multiselectable меняет не только объявление, но и поведение
клавиатуры, и это требование APG,
а не решение библиотеки.
| стрелка | пробел | |
|---|---|---|
| Без атрибута | двигает фокус и выбор | ничего нового |
aria-multiselectable="true" |
двигает только фокус | включает и выключает |
Иначе пройти полосу стрелками значило бы включить всё, мимо чего прошёл.
События
inst:select всплывает с переключённого чипа. В detail.selected — стало
включено или выключено; код — на вкладке JS у примера в шапке.
Опции
| Атрибут | Что делает |
|---|---|
data-value |
Значение в detail вместо подписи чипа |
aria-orientation="vertical" |
Стрелки по вертикали. Для полосы, поставленной столбиком |
API
| Имя | Что делает |
|---|---|
| класс | |
inst-chips | Полоса. Несёт role="listbox" и переносится по ширине |
inst-chip | Один фильтр. Несёт role="option" |
| Имя | Значение | Что делает |
|---|---|---|
| атрибут | ||
aria-orientation | horizontal | На .inst-chips. Обязателен: полоса идёт вбок, а listbox по умолчанию вертикальный |
aria-multiselectable | true | На .inst-chips. Включает несколько сразу: стрелка тогда двигает только фокус, а включает пробел |
aria-selected | — | true · false на каждом чипе. Носитель выбора |
data-value | — | На .inst-chip. Что придёт в detail события inst:select вместо текста |
aria-disabled | true | Фильтр недоступен: например, под него нет ни одной строки |
| Имя | Значение | Откуда |
|---|---|---|
| токен | ||
--control-h-sm | 24px | база +14: плотность · масштаб |
--radius-sm | 6px | база +4: масштаб |
--text-xs | 0.78125rem | база +4: масштаб |
--space-2 | 4px | одно на все ячейки |
--space-3 | 6px | одно на все ячейки |
--surface-recessed | light-dark(oklch(0 0 0 / 0.060), oklch(1 0 0 / 0.050)) | одно на все ячейки |
--accent-text | light-dark(var(--a-5), var(--a-3)) | база +1: тема |
--accent-border | light-dark(var(--a-4), var(--a-3)) | одно на все ячейки |
--border | light-dark(oklch(0 0 0 / 0.12), oklch(1 0 0 / 0.11)) | одно на все ячейки |
--hairline | 1px | база +1: экран |
Почему кит устроен именно так — конституция · Открытый код под MIT, github.com/keshon/instrument