HyMultiCombobox
Usage examples on this page are written for Lit / plain HTML (
<hy-multi-combobox>). The same component ships asHyMultiComboboxin@whitespaceux/harmony-react(native React 19), with typed wrappers for Angular, Solid, Svelte, and Vue. The API reference below applies to all frameworks.
A multi-combobox — a filterable multi-select that combines a text input with a listbox popup and a row of selected-value chips. Sibling to hy-combobox; a separate element rather than a multiple flag because multi-select carries a different internal model (array value, chip rendering, toggle-on-Enter keyboard semantics, multi-entry form participation) that would otherwise spread conditionals throughout the single-select implementation.
Follows the WAI-ARIA APG combobox pattern: DOM focus stays on the input, option highlight moves via aria-activedescendant. Enter toggles the highlighted option without closing the listbox; Backspace on an empty input removes the last selected chip.
The listbox is positioned with Floating UI: offset + flip + shift keep the popup inside the viewport near the input, and the size middleware caps the listbox height to the available space. autoUpdate repositions on scroll/resize while the listbox is open. The popup opens at least as wide as the field and grows to fit its longest row (a description, a trailing text), up to --hy-multi-combobox-surface-width-md and never past the viewport; past the cap a label truncates and a description wraps. A field wider than the cap keeps its own width.
Selected values render as dismissible <hy-chip> elements ahead of the text input. Chips wrap to multiple rows when the input row fills; long labels truncate with ellipsis. An optional maxSelections caps the count — once reached, unselected options appear disabled in the listbox.
Options are passed as a property (.options = [...]). The values property is the currently committed option values (empty array when none). Filtering is controlled internally via filterOption; return false for every option to drive results server-side.
Option fields. Besides value / label / disabled, an option can carry a sample (one leading visual — a color square, a glyph, an avatar or text in its own face; see PickerSample), a description (a subdued second line, exposed as aria-description on the row), a group (consecutive options sharing a group render under one non-selectable header; the picker never re-sorts), a disabledReason (shown under a disabled option and folded into its aria-description) and a trailing text at the end of the row. A selected value's chip carries its sample through the chip's prefix slot. A person's avatar sample takes presence, categoryGroup and identity as any hy-avatar does — pass the identity the rest of the app uses, so the person has one color everywhere; the status dot shows in the list only, never on a chip, and a status that matters goes into the description too. Filtering matches on label only — widen it through filterOption.
Width. The field fills its container. A field narrower than that states how much room it gives the typing with chars (1–8, 10, 12, 16, 20, 24, 32, 48): the chip row is that many characters wide, so the input always has that room on a line of its own and chips wrap above it as they do today (a chip longer than the room widens the row, up to the chip's own cap); the component adds its padding, border, chevron and clear button. width sets a fixed CSS width instead. Neither grows past the container.
Accessible name. Provide one of: the label property, aria-label on the host, or aria-labelledby referencing another element. The dev-mode WCAG 1.3.1 warning fires only when none is set; aria-label / aria-labelledby on the host are forwarded to the inner <input>.
Select-only mode (select-only). The same picker with the text input replaced by a button after the chips — the APG select-only combobox, multi flavored: nothing is typed and nothing filters; focus does not open the list; a click / Enter / Space / an arrow / Home / End do; Enter / Space toggle the highlighted row and the list stays open for the next pick; Tab and Alt+Up close it; Escape closes, or clears everything when the list is closed; Backspace removes the last chip; PageUp / PageDown move by ten; typing jumps to the next option whose label starts with what was typed (a repeated letter cycles). The picker for a set that must look the same in every browser and is picked, never searched.
Creatable (creatable). The user can add a value the options don't have: typing shows a "Create “…”" row at the end of the list (create-label words it, {value} marks where the typed text goes), and choosing it adds the trimmed text as a value — a chip, a form entry, a change. The row is a real option, so the keyboard and screen readers reach it as any other; it is highlighted by itself only when nothing else matches, so Enter on a partial match still picks the match. No row for text that matches an option's label or a value already chosen (ignoring case). A created value is not added to options — keep it there yourself if it should stay pickable. A cancelable create event fires first: call preventDefault() to refuse it (a malformed email, a banned word). Ignored in select-only mode, where nothing is typed.
When to use
- Picking several values from a long list: tags, recipients, assignees.
- Adding values that are not in the list yet, with
creatable.
Do
- Keep each value short enough to read in its chip.
- Set
empty-textto say what to do when nothing matches.
When not to use
- Only one value allowed:
hy-combobox. - A few options that fit on screen:
hy-checkbox-group, so every option and its state stay visible. - Free entry with nothing to pick from:
hy-text-input, orhy-text-areawith one value per line.
Accessibility: your part
- Name the field:
label, oraria-label/aria-labelledbyon the host when a dialog title or nearby text already names it. - For people options, put a status that matters in the option's
descriptiontoo ("Away"). The avatar sample is not announced.
Examples
<hy-multi-combobox label="Countries" placeholder="Search countries"></hy-multi-combobox>
<script>
const box = document.querySelector('hy-multi-combobox');
box.options = [
{ value: 'ch', label: 'Switzerland' },
{ value: 'fr', label: 'France' },
{ value: 'de', label: 'Germany' },
];
box.addEventListener('change', (e) => console.log(e.detail.values));
</script>API
Properties
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
options | — | MultiComboboxOption[] | [] | Option list. Property-only — arrays are not reflectable. |
values | — | string[] | [] | Committed values in selection order. Property-only — arrays are not reflectable. |
label | label | string | '' | Visible label above the input. |
helperText | helper-text | string | '' | Helper text below the input. Replaced by the error or warning message while one shows. |
errorMessage | error-message | string | '' | Error message below the input (replaces the helper text). Setting it raises error. |
warningMessage | warning-message | string | '' | Warning message below the input (replaces the helper text). Setting it raises warning. |
placeholder | placeholder | string | '' | Placeholder shown when the input is empty and no chips are selected. |
size | size | 'small' | 'medium' | 'large' | 'medium' | Input size — matches sibling form controls. |
layout | layout | 'block' | 'inline' | 'block' | Row layout: block (default) stacks label above the combobox; inline puts label on the left and combobox on the right. Helper/error always sit under the control (col 2–end), never under the label. Set --hy-multi-combobox-label-column-width on a wrapping container so stacked comboboxes align. |
width | width | string | undefined | — | A fixed width for the field, as any CSS width value (e.g. '400px', '20rem') — for a size the layout dictates rather than the content. Never wider than the container. To size a field to its content, use chars. Mirrors hy-text-input. |
chars | chars | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 10 | 12 | 16 | 20 | 24 | 32 | 48 | undefined | How many characters of room the field gives the typing — the way to make a field narrower than its container. One of 1–8, 10, 12, 16, 20, 24, 32, 48. The chip row is that many 0 glyphs wide in the field's own font, so the input (or the select-only trigger) holds that many characters on a line of its own while chips wrap above it (a chip longer than the room widens the row, up to the chip's own cap); padding, border, chevron and clear button add on top, so it follows every design system. Never wider than the container. A value outside the set is ignored (dev warning). width wins when both are set. |
name | name | string | '' | Form name — surfaces the selected values to FormData as repeated entries. |
disabled | disabled | boolean | false | Disables the control. |
required | required | boolean | false | Marks the field as required for form validation (at least one selection). |
error | error | boolean | false | Error state: the value is not accepted. Paints the danger border and sets aria-invalid; without error-message the helper text stays. Visual and for screen readers only — validity comes from required. |
warning | warning | boolean | false | Warning state: the value is accepted but worth a second look. Paints the warning border at rest and shows warning-message; never sets aria-invalid and never blocks submission. error wins when both are set. |
clearable | clearable | boolean | true | Whether to show a clear-all button when at least one value is set. On by default; turn it off from markup with clearable="false" (a bare clearable attribute, or none at all, keeps it on). |
clearLabel | clear-label | string | 'Clear all selections' | Accessible name of the clear button, which removes every selection. Use for internationalization. |
emptyText | empty-text | string | 'No results' | Fallback text when no options match the current query. |
chipRemoveLabel | chip-remove-label | string | 'Remove {label}' | Accessible name of each chip's remove button; {label} is replaced by the option's label (Remove {label}). Use for internationalization. |
openOptionsLabel | open-options-label | string | 'Open options' | Accessible name of the expand button while the popup is closed. Use for internationalization. |
closeOptionsLabel | close-options-label | string | 'Close options' | Accessible name of the expand button while the popup is open. Use for internationalization. |
requiredLabel | required-label | string | 'required' | The word announced for the required indicator (*) after the label. Use for internationalization. |
open | open | boolean | false | Controlled open state. Leave unset for uncontrolled behavior. |
maxSelections | max-selections | number | 0 | Maximum number of selections. When reached, unselected options render as disabled in the listbox. 0 or unset means unlimited. |
creatable | creatable | boolean | false | Lets the user add a value the options don't have: typing offers a "Create “…”" row at the end of the list. Ignored in select-only mode. |
createLabel | create-label | string | 'Create “{value}”' | Wording of the "Create" row; {value} is replaced by the typed text. Localize it here. |
autofocus | autofocus | boolean | false | Marks this multi-combobox as the auto-focus target. Read by hy-dialog (and other overlay containers) on show to choose initial focus. The marker on the host applies even though the inner typeahead input is what actually receives focus. Also focuses on first connect when used outside an overlay, mirroring native HTML autofocus but routed through JS so SSR'd HTML doesn't pull focus during hydration. |
autoselect | autoselect | boolean | false | After auto-focus lands, selects the typeahead input's current text (typically empty). Pairs with autofocus for symmetry with the single-select combobox. |
filterOption | — | MultiComboboxFilter | defaultFilter | Custom filter. Called for each option; return true to include. Default is a case-insensitive substring match on label. |
selectOnly | select-only | boolean | false | Select-only mode: a button trigger after the chips instead of the text input — no typing, no filtering, the APG select-only keyboard contract (see the class notes). filterOption, autoselect and the input event are inert in this mode. |
form | — | HTMLFormElement | null | — | — |
validity | — | ValidityState | — | — |
validationMessage | — | string | — | — |
Events
| Event | Detail | Description |
|---|---|---|
change | — | Fires when the selection changes. Detail: MultiComboboxChangeDetail. |
input | — | Fires while the user types. Detail: MultiComboboxInputDetail. |
create | — | Fires before a typed value is added in creatable mode; cancelable — preventDefault() refuses it. Detail: MultiComboboxCreateDetail. |
show | — | Fires when the listbox opens (does NOT bubble). Detail: HyVisibilityDetail. |
hide | — | Fires when the listbox closes (does NOT bubble). Detail: HyVisibilityDetail. |
CSS Parts
| Part | Description |
|---|---|
base | Outer wrapper containing label, control, helper/error. |
label | The <label> element. |
container | The control row holding chips, input, and trailing icons. |
chips | Wrapper around selected-value chips. |
chip | An individual selected-value chip. |
input | The text <input>. |
trigger | The <button> that replaces the input in select-only mode (the chips stay). |
clear-button | The clear affordance (clears all chips when set). |
expand-icon | The chevron indicating popup state. |
listbox | The popup <ul role="listbox">. |
option | An individual <li role="option">. |
option-check | The inline check mark on selected options. |
create-option | The "Create “…”" row in creatable mode (also an option). |
sample | The sample cell on an option row or a chip (hy-swatch, hy-icon, hy-avatar or a text span, by kind). |
option-description | The subdued second line of an option. |
option-reason | The disabled reason under a disabled option. |
option-trailing | The trailing text at the end of an option. |
group-header | A non-selectable section header (<li role="presentation">). |
empty | The no-results message container. |
helper-text | Helper text below the input. |
error-message | Error message below the input. |
warning-message | Warning message below the input. |
message-icon | The icon starting the error or warning message. |
CSS Custom Properties
The documented hooks. Every override hook the component reads — one per token it paints — is listed in @whitespaceux/harmony/theme-api.json, and @whitespaceux/harmony/component-bindings.json (the Harmony MCP get-bindings tool) says which part and state each one paints.
| Property | Default | Description |
|---|---|---|
--hy-multi-combobox-listbox-max-rows | — | Plain rows the popup shows before it scrolls (a number, not a length — the cap lands on a row edge). |
--hy-multi-combobox-gap-row-list-xs-block | var(--hy-gap-row-list-xs-block) | Gap between two option rows, so a hovered row never touches a selected one (default: the row-list xs gap, the sidebar's 4 px at standard). |
--hy-multi-combobox-listbox-max-height | — | Maximum height of the popup before scrolling; overrides the row count with a length. The runtime keeps the smaller of this and the viewport space (Floating UI's size middleware). |
--hy-multi-combobox-surface-width-md | var(--hy-surface-width-md) | Widest the popup grows to fit its rows. The popup is never narrower than the field (a wider field wins) and never wider than the viewport space. |
--hy-multi-combobox-label-column-width | — | Width of the label column in layout="inline" mode. No default — set on a wrapping container so stacked comboboxes align. |
--hy-multi-combobox-control-height-sm | var(--hy-control-height-sm) | Outer height for small size variant. Per-component override in the form-control height contract. Multi-combobox is the only form control that applies this as min-height rather than height — chips can wrap to multiple rows above the floor. |
--hy-multi-combobox-control-height-md | var(--hy-control-height-md) | Outer height for medium size variant. |
--hy-multi-combobox-control-height-lg | var(--hy-control-height-lg) | Outer height for large size variant. |
--hy-multi-combobox-motion-transition-hover-duration | var(--hy-motion-transition-hover-duration) | Transition duration for hover and focus state feedback |
--hy-multi-combobox-motion-transition-hover-easing | var(--hy-motion-transition-hover-easing) | Transition easing for hover and focus state feedback |
--hy-multi-combobox-motion-transition-expand-duration | var(--hy-motion-transition-expand-duration) | Transition duration for the listbox expand and collapse animation |
--hy-multi-combobox-motion-transition-expand-easing | var(--hy-motion-transition-expand-easing) | Transition easing for the listbox expand and collapse animation |
--hy-multi-combobox-indicator-size-md | var(--hy-indicator-size-md) | Edge of the sample cell on option rows. |
--hy-multi-combobox-indicator-size-sm | var(--hy-indicator-size-sm) | Edge of the sample cell inside a selected-value chip. |
--hy-multi-combobox-description-foreground-subtle-rest | var(--hy-foreground-subtle-rest) | Ink of the option description line. |
--hy-multi-combobox-group-foreground-subtle-rest | var(--hy-foreground-subtle-rest) | Ink of a group header. |
--hy-multi-combobox-border-warning-rest | var(--hy-border-warning-rest) | Border color in the warning state, at rest. |
--hy-multi-combobox-foreground-warning-rest | var(--hy-foreground-warning-rest) | Warning message text and icon color. |
--hy-multi-combobox-gap-layout-2xs | var(--hy-gap-layout-2xs) | Gap between the message icon and its text. |
--hy-multi-combobox-group-label-text-transform | none | Case of the option group headers in the list (none unless set: shown as written; uppercase for capitals). Display only — the header text stays as written. |
Methods
select()
Selects the typeahead input's current text. The chip values are not affected — they are not text content.