v1.0.0

<bmx-chip-set>

A row of chips that behaves as one thing.

9 properties · 2 events · 1 methods · 2 parts

Example

Both of these are one element with a JSON 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.
Tab into the row once, then use the arrows. 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 <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.
The +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 &mdash; 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>&lt;body&gt;</code>, which throws a keyboard user back to the
    top of the document once per removal &mdash; 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>&ldquo;2 more: Priya Raman, Tom
    Baird&rdquo;</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 &mdash; 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

PropertyAttributeTypeDefaultDescription
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

EventDetailDescription
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

MethodSignatureDescription
setFocus setFocus() => Promise<void> Focus the set's active chip, or the set itself when it is empty.

Slots

SlotDescription
(default) bmx-chip elements.

CSS shadow parts

PartDescription
more The counter chip.
set The row.

CSS custom properties

PropertyDescription
--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.