Skip to content
RU
Scale
Density
Theme
Accent

Actions

Chip

JS Toggling, arrows and the roving tabindex are done by `instrument.js`

A filter that is visible and is switched on by pressing. Several are on at once — and that is the difference from the segmented control, where exactly one value is always chosen.

The component
running failed queued done cancelled
Code
<div class="inst-chips" role="listbox" aria-multiselectable="true"
     aria-orientation="horizontal" aria-label="Filter by state">
  <span class="inst-chip" role="option" aria-selected="true" tabindex="0">running</span>
  <span class="inst-chip" role="option" aria-selected="true" tabindex="-1">failed</span>
  <span class="inst-chip" role="option" aria-selected="false" tabindex="-1">queued</span>
  <span class="inst-chip" role="option" aria-selected="false" tabindex="-1">done</span>
  <span class="inst-chip" role="option" aria-selected="false" aria-disabled="true" tabindex="-1">cancelled</span>
</div>

Contract

What Required Why
role="listbox" on the strip yes Without it role="option" is invalid, and the arrows will not work
aria-label on the strip yes "Filter by state": otherwise a screen reader announces a list with no name
aria-selected on every chip yes The carrier of the choice. false is written too — the absence of the attribute means "no choice is provided here"
tabindex yes, the initial one Zero on one, -1 on the rest. After that it is led by instrument.js. Without a roving tabindex Tab goes through every chip and the strip stops being one control
aria-orientation="horizontal" yes The strip runs sideways, and a listbox is vertical by default: without the attribute the traversal would hang on ↑ ↓ while the row runs to the right
aria-multiselectable="true" no Set when several are on at once. Without it the strip behaves as a single-choice list: the selection follows the focus

The keyboard: Tab enters and leaves the strip, ← → traverse it, space switches on and off.

A selected chip carries two marks rather than one colour: the colour of the label and the fill. Hover takes a third channel — the border — and so does not argue with the selection. The label of an unselected one is --text-secondary, of a selected one --accent-text, and both are checked at 4.5:1 in five themes and four accents. The tap target is the height --control-h-sm with the strip's gap of --space-2, checked by cmd/targets in all fifteen combinations of scale and density.

The inst:select event arrives with detail.selected — switched on or off. A cancelled event leaves the markup untouched: the application has taken the state on itself and will set aria-selected itself.

States

A chip has no tone. It names a filter rather than a state: painting "failed" red would say the filter itself is the emergency. Colour in a chip means exactly one thing — whether it is on.

Three channels, and they do not overlap, so they read together:

Channel What it encodes
The colour of the label selected — --accent-text, not — --text-secondary
The colour of the border under the cursor — --accent-border, otherwise — --border
The fill present when selected or under the cursor

The weight deliberately does not change on selection: the strip wraps across the width, and a label grown thicker would move its neighbours onto another line at the moment of the press.

JS

The module is included once per page — there is no need to initialise the components one by one.

What instrument.js does

The examples on this page are live: enter the strip with Tab, then ← and →, switching on with space. With a mouse too.

The strip is declared as role="listbox", and instrument.js carries out its contract: ← → between the chips, Home and End, one Tab for the whole strip.

Single and multiple

The aria-multiselectable attribute changes not only the declaration but the behaviour of the keyboard, and that is a requirement of APG rather than a decision of the library.

arrow space
Without the attribute moves the focus and the selection nothing new
aria-multiselectable="true" moves the focus only switches on and off

Otherwise going through the strip with arrows would mean switching on everything passed along the way.

Events

inst:select bubbles from the toggled chip. In detail.selected is whether it became on or off; the code is on the JS tab of the example in the header.

Options

Attribute What it does
data-value The value in detail instead of the chip's label
aria-orientation="vertical" Arrows on the vertical. For a strip set in a column

API

Classes
NameWhat it does
class
inst-chipsThe strip. It carries role="listbox" and wraps across the width
inst-chipOne filter. It carries role="option"
Attributes
NameValueWhat it does
attribute
aria-orientationhorizontalOn .inst-chips. Obligatory: the strip runs sideways, and a listbox is vertical by default
aria-multiselectabletrueOn .inst-chips. It switches several on at once: an arrow then moves only the focus, and space switches on
aria-selected—true · false on every chip. The carrier of the choice
data-value—On .inst-chip. What arrives in the detail of inst:select instead of the text
aria-disabledtrueThe filter is unavailable: there is not one row under it, for instance
Tokens
NameValueWhere from
token
--control-h-sm24pxbase +14: density · scale
--radius-sm6pxbase +4: scale
--text-xs0.78125rembase +4: scale
--space-24pxone across every cell
--space-36pxone across every cell
--surface-recessedlight-dark(oklch(0 0 0 / 0.060), oklch(1 0 0 / 0.050))one across every cell
--accent-textlight-dark(var(--a-5), var(--a-3))base +1: theme
--accent-borderlight-dark(var(--a-4), var(--a-3))one across every cell
--borderlight-dark(oklch(0 0 0 / 0.12), oklch(1 0 0 / 0.11))one across every cell
--hairline1pxbase +1: screen

Why the kit works this way — the constitution · Open source under MIT, github.com/keshon/instrument