Multicombobox
hc-multicombobox is a multi-select combobox with a tag-input
control. Selected values render as inline chips inside a single
visual surface, the filter input lives next to them, and the
listbox carries aria-multiselectable="true" so it doesn’t close
after each pick.
Same architectural primitives as
hc-combobox —
WAI-ARIA 1.2 combobox pattern, HTML popover, CSS Anchor
Positioning, aria-activedescendant for the highlighted option
while DOM focus stays on the input.
Basic example
Section titled “Basic example”- JavaScript
- TypeScript
- Python
- Go
- Rust (coming soon)
<div class="hc-multicombobox" data-name="languages"> <div class="hc-multicombobox__control hc-input"> <span class="hc-multicombobox__tags"></span> <input class="hc-multicombobox__input" type="text" role="combobox" aria-controls="lang-list" aria-label="Languages"> </div>
<ul class="hc-multicombobox__listbox" id="lang-list" role="listbox"> <li class="hc-multicombobox__option" role="option" data-value="js">JavaScript</li> <li class="hc-multicombobox__option" role="option" data-value="ts">TypeScript</li> <li class="hc-multicombobox__option" role="option" data-value="py" aria-selected="true">Python</li> <li class="hc-multicombobox__option" role="option" data-value="go">Go</li> <li class="hc-multicombobox__option" role="option" data-value="rs" aria-disabled="true">Rust</li> </ul></div>import { installMulticombobox } from '@hypermedia-components/core';installMulticombobox();The pre-selected aria-selected="true" option becomes the
seeded tag.
What installMulticombobox() does
Section titled “What installMulticombobox() does”- ARIA:
aria-multiselectable="true"on the listbox,aria-haspopup="listbox",aria-autocomplete="list",aria-expanded,aria-controls(if missing),aria-activedescendanttracking the highlighted option. - Anchor binding: inline
anchor-nameon the input,position-anchoron the listbox. JS positioning fallback for browsers without CSS Anchor Positioning. - Seeding: every option with
aria-selected="true"at install time becomes a tag chip — keeps SSR-pre-selected state trivial. - Filtering: case-insensitive substring on each keystroke;
.hc-multicombobox__emptyplaceholder when nothing matches. - Toggle selection: click or Enter on a highlighted option toggles its selected state. The listbox stays open so several picks can be made without reopening.
- Tag removal:
- Click the
×button on a tag. - Press
Backspacewhile the input is empty to drop the last tag (the standard tag-input convention).
- Click the
- Form integration: when the wrapper has
data-name="X", the behavior writes one<input type="hidden" name="X" value="…">per selected value inside the wrapper. The form serialises like a native<select multiple name="X">. - Events: every state change dispatches
hc:multicomboboxchangeon the input withdetail.{values, added, removed, input}.
Keyboard
Section titled “Keyboard”| Key | Action |
|---|---|
↓ / ↑ | Open the listbox / move the activedescendant. |
Home / End | First / last enabled visible option. |
Enter | Toggle the highlighted option. |
Backspace (input empty) | Remove the last tag. |
Escape | Close the listbox; selections preserved. |
Tab | Close the listbox; normal tab order. |
Form integration
Section titled “Form integration”Set data-name on the wrapper for native form serialisation:
<form action="/save" method="post"> <div class="hc-multicombobox" data-name="languages">…</div> <button class="hc-button">Save</button></form>Submits as languages=js&languages=ts&languages=py — the same shape
a native <select multiple name="languages"> would produce. PHP /
Rails / Python frameworks parse this without configuration.
htmx usage
Section titled “htmx usage”The change event carries the full state, so an htmx swap on every edit is one trigger declaration:
<div class="hc-multicombobox" data-name="languages" data-hx-post="/profile/languages" data-hx-trigger="hc:multicomboboxchange from:closest .hc-multicombobox" data-hx-include="this" data-hx-target="#status"> …</div>Creatable
Section titled “Creatable”Add data-allow-create to let users add tags that aren’t in the list. When
the typed text has no exact match, a synthetic “Add …” option appears;
choosing it (click or Enter) creates a tag from the raw text and
fires hc:multicomboboxchange with the new value in added.
<div class="hc-multicombobox" data-name="tags" data-allow-create>…</div>The created tag’s label is the value itself, and a hidden name input is
added like any other tag. The option label is translatable via the
i18n catalog (multicombobox.create).
Rich options
Section titled “Rich options”Options can hold any HTML (icon + label + description). Use data-label
for the tag’s label and the matched value, and data-search for the
filter haystack (aliases/keywords), so the rich markup’s text doesn’t pollute
either:
<li class="hc-multicombobox__option" role="option" data-value="py" data-label="Python" data-search="python py snake"> <strong>Python</strong> <small>scripting</small></li>If you set a display on options for the rich layout, scope it to
:not([hidden]) so it doesn’t override the filter’s hide.
Accessibility
Section titled “Accessibility”- DOM focus stays on the input so the type-ahead anchor is
consistent —
aria-activedescendantmoves the highlight, not the focus. - Each tag is a real focusable button with
aria-label="Remove …"so screen-reader users can land on it and trigger removal. - Mark unavailable rows with
aria-disabled="true"rather thandisabled(<li>is not a form control). - The visible activedescendant highlight uses
--hc-color-focus-ringand followsdata-colorthemes.
Out of scope (MVP)
Section titled “Out of scope (MVP)”- Drag-to-reorder tags.
- Async option loading — the docs above show the htmx swap pattern; a built-in debounce / cancel helper is not bundled.
Theming tokens
Section titled “Theming tokens”Component tokens (in component.tokens.json):
| Token path | Purpose |
|---|---|
multicombobox.control.padding-x / padding-y / gap / min-height | Tag-input control box. |
multicombobox.input.min-width / fg | Inline text input. |
multicombobox.tag.bg / fg / border / padding-x / padding-y / radius / font-size / gap | Tag chip. |
multicombobox.tag.remove-fg / remove-hover-fg | × button. |
multicombobox.listbox.* | Mirror of the combobox listbox tokens. |
multicombobox.option.{padding-x, padding-y, font-size, fg, hover-bg, active-bg, check-color, disabled-fg, indicator-size} | Option layout + check mark. |
multicombobox.empty-fg | ”No matches” placeholder color. |