<bmx-chip-set>
A row of chips that behaves as one thing.
9 properties · 2 events · 1 methods · 2 parts
Example
items attribute and no script at all — which is what a
Razor page, a Blazor component, a Django template or an htmx swap can actually produce. Slot your own
bmx-chip children in instead when you want to control each one; the two are alternatives, and
items is ignored when anything is slotted.
<body>, which throws a keyboard user back to the
top of the document once per removal — it is the commonest keyboard failure in this pattern and it is
invisible to anybody testing with a mouse.
+2 counter is drawn and never read: what it announces is “2 more: Priya Raman, Tom
Baird”. Past five names it becomes a plain number, because a list of forty read aloud stops being an
answer. And a limit that would hide exactly one hides none — a +1 takes the same room as the
chip it replaced and tells you less.
Show markup
<bmx-chip-set
label="Filters"
selectable
items='[{"label":"Open","value":"open","selected":true},{"label":"Assigned to me","value":"mine"},{"label":"Needs review","value":"review"},{"label":"Blocked","value":"blocked"}]'
></bmx-chip-set>
<div class="row" style="margin-block-start: 1.5rem">
<bmx-chip-set
label="Recipients"
removable
max="4"
items='[{"label":"Amara Okonkwo","value":"amara"},{"label":"Grace Hopper","value":"grace"},{"label":"Dana Scott","value":"dana"},{"label":"Eli Cohen","value":"eli"},{"label":"Priya Raman","value":"priya"},{"label":"Tom Baird","value":"tom"}]'
></bmx-chip-set>
</div>
<div class="row" style="margin-block-start: 1.5rem">
<span class="note">
Both of these are one element with a JSON <code>items</code> attribute and no script at all — which is what a
Razor page, a Blazor component, a Django template or an htmx swap can actually produce. Slot your own
<code>bmx-chip</code> children in instead when you want to control each one; the two are alternatives, and
<code>items</code> is ignored when anything is slotted.
</span>
</div>
<div class="row" style="margin-block-start: 1rem">
<span class="note">
<strong>Tab into the row once, then use the arrows.</strong> Remove a chip and watch where the focus goes: to the
chip that moved into the space, to the previous one when you removed the last, and to the row itself when you empty
it. The browser's own answer to all three is <code><body></code>, which throws a keyboard user back to the
top of the document once per removal — it is the commonest keyboard failure in this pattern and it is
invisible to anybody testing with a mouse.
</span>
</div>
<div class="row" style="margin-block-start: 1rem">
<span class="note">
The <code>+2</code> counter is drawn and never read: what it announces is <em>“2 more: Priya Raman, Tom
Baird”</em>. Past five names it becomes a plain number, because a list of forty read aloud stops being an
answer. And a limit that would hide exactly one hides none — a <code>+1</code> takes the same room as the
chip it replaced and tells you less.
</span>
</div>
<!-- the consumer's own chips -->
<bmx-chip-set label="Filters">
<bmx-chip selectable value="open">Open</bmx-chip>
<bmx-chip selectable value="mine">Assigned to me</bmx-chip>
</bmx-chip-set>
<!-- or from data -->
<bmx-chip-set label="Recipients" removable max="4"></bmx-chip-set>
WHAT A SET IS FOR, GIVEN THAT A CHIP ALREADY WORKS ALONE
Three things, and every one of them is a keyboard problem that is invisible to anybody testing with a mouse.
One Tab stop instead of twelve. Eight recipients that are each
separately tabbable cost a keyboard user eight presses to get past the
field. WAI-ARIA's answer is a roving tabindex, which is what this component
implements over src/core/roving.ts - the same arithmetic the toolbar, the
tab strip and the button group use.
Somewhere to put the focus after a removal. Pressing Backspace on a chip
destroys the element that had the focus, and the browser's answer to that is
<body> - so a user clearing four tags is thrown back to the top of the
document four times. Where it goes instead is focusAfterRemoval in
src/core/collection.ts, and it is decided before the removal happens.
A limit that is honest. Past max the tail collapses into a counter,
and the counter says how many and - when there are few enough for it to be a
sentence - which. +3 is a truncation, and this library does not read
truncations out.
TWO SOURCES, ONE CODE PATH
Chips come either from the consumer's own markup or from items. They are
alternatives rather than a mixture: items is ignored when anything is
slotted in, because a set that drew both would have two orders and no way to
say which came first. Everything below this point works over one array of
bmx-chip elements and does not know which of the two produced it.
WHOSE EVENTS ARE WHOSE
A chip the consumer wrote is the consumer's element, so its bmxChange and
bmxRemove reach them exactly as they would without a set around it -
stopping those would break a chip by putting it in a container. A chip this
component drew is not: the consumer never wrote it and cannot hold a
reference to it, so its events stop at the boundary and the set re-publishes
what belongs to its API. Either way bmxChange from the set carries the
whole selection, which is the thing a filter bar actually wants and which no
single chip can report.
Properties
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
expandable |
expandable |
boolean |
true |
Whether pressing the counter shows the rest. |
items |
property only | BmxChipEntry[] |
[] |
The chips, as data. Read through src/core/markup.ts, so items='[{"label":"Open"}]' written as an attribute by a Razor page, a Blazor component or an htmx swap is the list it looks like rather than a string the component tries to map over. |
label |
label |
string |
— | What the group is called. A row of chips with no name is announced as a group of buttons with no indication of what they filter. It is the one property here worth calling near-mandatory. |
max |
max |
number |
— | The most chips to draw before the rest collapse into a counter. A limit that would hide exactly one chip hides none: the counter takes the same room and says less. See src/core/collection.ts. |
moreLabel |
more-label |
string |
— | The counter's accessible name, when the composed one is not wanted. |
orientation |
orientation |
BmxOrientation |
'horizontal' |
Which arrow keys move between chips. |
removable |
removable |
boolean |
false |
Whether chips drawn from items offer a remove button. |
selectable |
selectable |
boolean |
false |
Whether chips drawn from items can be turned on and off. |
wrap |
wrap |
boolean |
true |
Whether the row wraps onto a second line rather than scrolling. |
Events
| Event | Detail | Description |
|---|---|---|
bmxChange |
BmxChipSetChangeDetail |
The selection changed. Carries the whole selection, not just the chip that moved. |
bmxRemove |
BmxChipSetRemoveDetail |
A chip drawn from items asked to be removed. Cancelable. Uncancelled, the set takes the entry out of items itself and moves the focus; prevented, nothing happens and the focus stays where it was. Chips the consumer slotted in emit their own bmxRemove and this one is not raised for them - the event already reached whoever wrote the chip. |
Methods
| Method | Signature | Description |
|---|---|---|
setFocus |
setFocus() => Promise<void> |
Focus the set's active chip, or the set itself when it is empty. |
Slots
| Slot | Description |
|---|---|
(default) |
bmx-chip elements. |
CSS shadow parts
| Part | Description |
|---|---|
more |
The counter chip. |
set |
The row. |
CSS custom properties
| Property | Description |
|---|---|
--bmx-chip-set-focus-ring |
The indicator on the set itself, which is focusable only when empty. |
--bmx-chip-set-gap |
The space between chips. |
--bmx-chip-set-row-gap |
The space between rows, when the set wraps. |