Combobox
Combine editable text input with filtered, keyboard-navigable options.
Try it
<hui-combobox id="playground-combobox" name="plan" value="standard" aria-label="Plan" filter="contains" autocomplete="both">
<hui-combobox-input placeholder="Search plans"></hui-combobox-input>
<hui-combobox-trigger aria-label="Show plans">Options</hui-combobox-trigger>
<hui-combobox-content>
<hui-combobox-option value="basic">Basic</hui-combobox-option>
<hui-combobox-option value="standard">Standard</hui-combobox-option>
<hui-combobox-option value="pro">Professional</hui-combobox-option>
<hui-combobox-empty>No plans found.</hui-combobox-empty>
<hui-combobox-status>Loading plans...</hui-combobox-status>
</hui-combobox-content>
</hui-combobox>- data-state
- Waiting for enhancement
No component events yet.
Usage
Use hui-combobox when users need to type a query and choose one or more options from a managed list. Use a select when free-text filtering is not needed. Do not use it as a generic search field without options or as a replacement for a command menu.
Anatomy
- Root:
hui-combobox - Input and trigger:
hui-combobox-input,hui-combobox-trigger - Popup:
hui-combobox-content - Options and feedback:
hui-combobox-option,hui-combobox-empty,hui-combobox-status
Examples
<section aria-labelledby="combobox-heading">
<h2 id="combobox-heading">Search plans</h2>
<form id="combobox-form">
<hui-combobox id="plan-combobox" name="plan" value="standard" aria-labelledby="combobox-heading" autocomplete="both" loop>
<hui-combobox-input placeholder="Search plans"></hui-combobox-input>
<hui-combobox-trigger aria-label="Show plan options"><span aria-hidden="true">▾</span></hui-combobox-trigger>
<hui-combobox-content>
<hui-combobox-option value="basic">Basic</hui-combobox-option>
<hui-combobox-option value="standard"><b>Standard</b></hui-combobox-option>
<hui-combobox-option value="pro" disabled>Professional</hui-combobox-option>
<hui-combobox-option value="cafe" label="Café">Café</hui-combobox-option>
<hui-combobox-empty>No matching plans.</hui-combobox-empty>
<hui-combobox-status>Loading plans…</hui-combobox-status>
</hui-combobox-content>
</hui-combobox>
<h3 id="multi-combobox-heading">Selected plans</h3>
<hui-combobox id="multi-plan-combobox" name="plans" value="basic,standard" selection="multiple" aria-labelledby="multi-combobox-heading">
<div data-part="selected-values" role="list" aria-label="Selected plans">
<div data-part="token" data-value="basic" role="listitem">
<span>Basic</span>
<button type="button" data-remove-value aria-label="Remove Basic">×</button>
</div>
<div data-part="token" data-value="standard" role="listitem">
<span>Standard</span>
<button type="button" data-remove-value aria-label="Remove Standard">×</button>
</div>
</div>
<hui-combobox-input placeholder="Add plans"></hui-combobox-input>
<hui-combobox-trigger aria-label="Show plan options"><span aria-hidden="true">▾</span></hui-combobox-trigger>
<hui-combobox-content>
<hui-combobox-option value="basic">Basic</hui-combobox-option>
<hui-combobox-option value="standard">Standard</hui-combobox-option>
<hui-combobox-option value="cafe" label="Café">Café</hui-combobox-option>
</hui-combobox-content>
</hui-combobox>
</form>
</section>API reference
hui-combobox
| Attribute | Meaning |
|---|---|
value | Committed option value. Multiple values are comma-separated when selection="multiple". |
default-value | Seeds value once when no value is present. |
query | Current editable query. The selected option label is displayed when the query is not dirty. |
default-query | Seeds query once when no query is present. |
open / default-open | Controls or seeds popup visibility. |
selection | single (default) or multiple. |
filter | auto (prefix), contains, or none. Matching is case- and accent-insensitive. |
autocomplete | list (default), both, or none; both enables inline completion. |
select-on-tab | Commits the highlighted option before normal tab navigation. |
enter-submit | Leaves Enter available for form submission while closed. |
loop | Wraps ArrowUp/ArrowDown highlight movement. |
busy | Exposes consumer-controlled loading state through aria-busy; it does not fetch results. |
placement / dir | Popup placement and keyboard/layout direction. |
name, form, disabled, required | Form and disabled-state metadata. |
id | Stable base for deterministic content and option IDs. ID-less roots omit generated references. |
aria-label, aria-labelledby, aria-describedby | Accessible naming inherited by the input and listbox. |
The root exposes data-state="open|closed", data-value, data-query, data-results, and data-empty. It emits hui-open-change and hui-value-change events with { open } and { value } details.
Input, trigger, and content
hui-combobox-input renders a native <input> with role="combobox", aria-expanded, aria-controls, aria-autocomplete, aria-haspopup="listbox", aria-activedescendant, and aria-busy. The input owns keyboard handling and retains focus. hui-combobox-trigger renders a labelled button that toggles the popup without stealing input focus.
hui-combobox-content renders role="listbox", inherits accessible naming, and is hidden while closed. Open content is positioned against the input and portalled when a clipped ancestor would contain it. After opening, page scrolling does not reposition the content; it stays at its last document coordinates, allowing the page to carry it out of the viewport. Opening and viewport resizing still run collision-aware positioning. data-state and data-placement are exposed.
For selection="multiple", consumers may project a selected-values surface. A token should expose its committed value through data-value and contain an accessible button[type="button"][data-remove-value]. The remove attribute may contain the value itself; when empty, the nearest token's data-value is used. The component owns the mutation and emits the existing hui-value-change event; consumers own token labels, layout, and rerendering. Remove controls are ignored for single-selection and disabled roots.
Options, empty, and status
hui-combobox-optionrendersrole="option",aria-selected,aria-disabled, a deterministic ID, anddata-highlighted.hui-combobox-emptyrenders an optionalrole="status"empty-result message.hui-combobox-statusrenders an optional polite status for loading or result metadata; result announcements also use the shared polite live-region helper.- Set
busywhile the consumer is fetching or replacing options. The input and listbox exposearia-busy="true", and a projectedhui-combobox-statusis visible only while the popup is open and busy. The component does not start a request or change popup visibility whenbusychanges. - The documentation playground's Show loading results control intentionally opens the popup when enabled so the status can be inspected; this convenience behavior is not part of the component API.
- Options use
value,label,disabled, and optionalselectedattributes. Whenvalueis omitted, option identity falls back to the hostid, then the option's positional index; use explicit values for collections whose order can change. Their projected text is the fallback accessible label.
States and styling
| State | Contract and styling guidance |
|---|---|
| Default | Closed, enabled input with the committed label or query visible. |
| Hover | Use --hui-color-interactive-hover for trigger or option feedback. |
| Active | Use --hui-color-interactive-active for pressed controls without changing the committed value until selection. |
| Focus | Keep the input or trigger focus ring visible with --hui-color-focus-ring. |
| Disabled | Expose native disabled semantics and use --hui-color-interactive-disabled; no popup interaction is allowed. |
| Error | No built-in validation state; pair aria-invalid="true" with error text and --hui-color-danger. |
Attributes are the source of truth. Style derived state with data-state, ARIA attributes, and the --hui-* token aliases.
Accessibility and keyboard support
- Typing filters options.
inputevents withisComposingdo not filter until composition ends.filter="auto"matches prefixes;containsmatches any substring;nonekeeps every option. - Closed
ArrowDown/ArrowUpopens and highlights the selected option, or the first/last enabled result. Open arrows move the highlight, skipping disabled or filtered options;loopwraps at the ends. - Closed
Home/Endretain native caret behavior. OpenHome/Endmove the highlight to the first/last enabled result. - Open Enter commits the highlighted option. Closed Enter remains available for form submission when
enter-submitis present. - Escape is staged: clear a non-selected highlight, then close, then restore the committed label/query.
Tabcommits the highlighted option whenselect-on-tabis present and always closes without trapping focus. - In
autocomplete="both", the first prefix match supplies a selected inline completion tail. ArrowRight and Tab accept the completion. - Pointer activation selects an option. Pointer-down keeps focus on the input. Outside pointer/focus and Escape dismiss the popup.
- Multiple selection toggles options without closing and keeps the input ready for another query.
- When a multiple-selection input is empty,
BackspaceorDeleteremoves the last committed value. With a non-empty query, both keys retain native text editing behavior.
SSR and forms
SSR renders the editable input, trigger, and a complete collapsed listbox with resolved option selection. It also renders a hidden form value input and a native datalist twin so the input remains useful without JavaScript. default-value, default-query, and default-open are resolved on the server. Closed content has hidden="hidden"; portal markers are client-only.
Hydration re-renders component-owned template nodes while preserving projected consumer content and host attributes. In an enhanced browser the custom listbox owns suggestions and the input's list association is removed, while the datalist node remains available in the DOM. Without JavaScript, the native input and its datalist remain usable; the custom listbox is hidden and the committed value is available through the hidden form input.
Code
<hui-combobox id="plan" name="plan" aria-label="Plan" filter="contains">
<hui-combobox-input placeholder="Search plans"></hui-combobox-input>
<hui-combobox-trigger aria-label="Show plans">Options</hui-combobox-trigger>
<hui-combobox-content>
<hui-combobox-option value="basic">Basic</hui-combobox-option>
<hui-combobox-option value="pro">Professional</hui-combobox-option>
<hui-combobox-empty>No plans found</hui-combobox-empty>
</hui-combobox-content>
</hui-combobox>
<hui-combobox id="playground-combobox" name="plan" value="standard" aria-label="Plan" filter="contains" autocomplete="both">
<hui-combobox-input placeholder="Search plans"></hui-combobox-input>
<hui-combobox-trigger aria-label="Show plans">Options</hui-combobox-trigger>
<hui-combobox-content>
<hui-combobox-option value="basic">Basic</hui-combobox-option>
<hui-combobox-option value="standard">Standard</hui-combobox-option>
<hui-combobox-option value="pro">Professional</hui-combobox-option>
<hui-combobox-empty>No plans found.</hui-combobox-empty>
<hui-combobox-status>Loading plans...</hui-combobox-status>
</hui-combobox-content>
</hui-combobox>