combo-box
Autocomplete combobox with filtering, keyboard navigation, and native form association. Supports single-select and multi-select tag modes.
Overview
An autocomplete combobox following the W3C ARIA combobox pattern. Users type to filter a list of options, navigate with arrow keys, and select with Enter or click. The selected value participates in native form submission via ElementInternals.
Add multiple to enable multi-select tag mode, where users can select multiple items as tag chips.
Usage
Place an <input> and a <ul> inside <combo-box>. Each <li> needs a data-value attribute for the form value. The visible text content becomes the label.
<combo-box name="country"> <input type="text" placeholder="Search countries..."> <ul> <li data-value="us">United States</li> <li data-value="gb">United Kingdom</li> <li data-value="ca">Canada</li> <li data-value="au">Australia</li> </ul></combo-box>
Inside a Form
Use the standard name attribute to set the form field name. The selected value is submitted as the form value. Add required to enable required validation.
<form id="my-form"> <combo-box name="fruit" required> <input type="text" placeholder="Pick a fruit..."> <ul> <li data-value="apple">Apple</li> <li data-value="banana">Banana</li> <li data-value="cherry">Cherry</li> </ul> </combo-box> <button type="submit">Submit</button></form> <script> document.getElementById('my-form') .addEventListener('submit', (e) => { e.preventDefault(); const data = new FormData(e.target); console.log('fruit:', data.get('fruit')); });</script>
Filter Modes
By default, typing filters options by substring match (contains). Set filter="startsWith" to only match from the beginning of each option.
<!-- Default: contains (substring match) --><combo-box name="item" filter="contains"> ...</combo-box> <!-- Starts-with matching --><combo-box name="item" filter="startsWith"> ...</combo-box>
Multi-Select Mode
Add multiple to switch to multi-select tag mode. Users select multiple items which appear as removable tag chips. Selected options are hidden from the dropdown to prevent duplicates.
<combo-box name="topics" multiple> <input type="text" placeholder="Search topics..."> <ul> <li data-value="js">JavaScript</li> <li data-value="css">CSS</li> <li data-value="html">HTML</li> </ul></combo-box>
Multi-Select Options
In multi mode, use max to limit the number of selections and custom to let users type custom entries (press Enter or comma to add). Each selected tag is submitted as a separate FormData entry (like multiple checkboxes with the same name).
<form id="my-form"> <combo-box name="skills" multiple required max="5" custom> <input type="text" placeholder="Add skills..."> <ul> <li data-value="design">Design</li> <li data-value="frontend">Frontend</li> <li data-value="backend">Backend</li> </ul> </combo-box> <button type="submit">Submit</button></form> <script> document.getElementById('my-form') .addEventListener('submit', (e) => { e.preventDefault(); const data = new FormData(e.target); // Multiple values under same name console.log('skills:', data.getAll('skills')); });</script>
Attributes
| Attribute | Values | Default | Description |
|---|---|---|---|
name | string | — | Form field name |
required | boolean | — | Require a selection for form validation |
filter | "contains", "startsWith" | contains | How typing filters the options |
value | string | — | Selected value (single mode) |
placeholder | string | — | Input placeholder text |
multiple | boolean | — | Enable multi-select tag mode |
max | number | — | Maximum number of tags (multi mode) |
custom | boolean | — | Allow typed entries via Enter/comma (multi mode) |
Required Structure
| Element | Required | Description |
|---|---|---|
<input type="text"> | yes | Text input for filtering and display |
<ul> or <ol> | yes | Options list container |
<li data-value> | yes | Individual option — one per selectable choice |
Child Attributes
| Attribute | On | Values | Description |
|---|---|---|---|
data-value | li | string | Option value identifier placed on each <li> in the options list |
Events
| Event | Detail | Description |
|---|---|---|
combo-box:change | { value, label } | Fired when an option is selected (single mode). |
combo-box:change | { values: string[], labels: string[] } | Fired when tags are added or removed (multi mode). |
combo-box:open | — | Fired when the listbox opens. |
combo-box:close | — | Fired when the listbox closes. |
Single Mode
const combo = document.querySelector('combo-box'); combo.addEventListener('combo-box:change', (e) => { console.log('Selected:', e.detail.value, e.detail.label);}); combo.addEventListener('combo-box:open', () => { console.log('Listbox opened');}); combo.addEventListener('combo-box:close', () => { console.log('Listbox closed');});
Multi Mode
const combo = document.querySelector('combo-box[multiple]'); combo.addEventListener('combo-box:change', (e) => { console.log('Values:', e.detail.values); console.log('Labels:', e.detail.labels);});
JavaScript API
| Property | Type | Mode | Description |
|---|---|---|---|
element.value | string | Single | Current selected value (read/write) |
element.label | string | Single | Current selected label (read-only) |
element.values | string[] | Multi | Array of selected tag values (read-only) |
element.labels | string[] | Multi | Array of selected tag labels (read-only) |
Single Mode
const el = document.querySelector('combo-box'); // Read current value and labelconsole.log(el.value); // "us"console.log(el.label); // "United States" // Set value programmaticallyel.value = 'gb'; // Clear selectionel.value = '';
Multi Mode
const el = document.querySelector('combo-box[multiple]'); // Read current values and labelsconsole.log(el.values); // ["js", "css"]console.log(el.labels); // ["JavaScript", "CSS"]/code-block </section> <section> <h2>Keyboard Navigation</h2> <table class="props-table"> <thead> <tr><th>Key</th><th>Action</th></tr> </thead> <tbody> <tr><td><kbd>ArrowDown</kbd></td><td>Open listbox / move to next option</td></tr> <tr><td><kbd>ArrowUp</kbd></td><td>Open listbox / move to previous option</td></tr> <tr><td><kbd>Home</kbd></td><td>Jump to first option</td></tr> <tr><td><kbd>End</kbd></td><td>Jump to last option</td></tr> <tr><td><kbd>Enter</kbd></td><td>Select option (single) or add tag (multi). With <code>custom</code>: add typed text.</td></tr> <tr><td><kbd>Escape</kbd></td><td>Close listbox</td></tr> <tr><td><kbd>Tab</kbd></td><td>Close listbox and move focus</td></tr> <tr><td><kbd>Backspace</kbd></td><td>Remove last tag when input is empty (multi mode)</td></tr> <tr><td><kbd>,</kbd> (comma)</td><td>Add custom tag (multi mode with <code>custom</code>)</td></tr> </tbody> </table> </section> <section> <h2>Accessibility</h2> <h3>ARIA Pattern</h3> <p>Implements the <a href="https://www.w3.org/WAI/ARIA/apg/patterns/combobox/">W3C ARIA Combobox pattern</a>:</p> <ul> <li>Input has <code>role="combobox"</code>, <code>aria-expanded</code>, <code>aria-autocomplete="list"</code>, and <code>aria-controls</code></li> <li>List has <code>role="listbox"</code> with an auto-generated ID</li> <li>Options have <code>role="option"</code> and <code>aria-selected</code></li> <li>Active option announced via <code>aria-activedescendant</code></li> <li>Multi mode adds <code>aria-multiselectable="true"</code> and an <code>aria-live</code> region for tag add/remove announcements</li> </ul> <h3>Form Validation</h3> <p>Uses <code>ElementInternals.setValidity()</code> so screen readers can announce validation errors. Works with <code>:user-valid</code> and <code>:user-invalid</code> CSS pseudo-classes.</p> </section> <section> <h2>Form Association</h2> <p>This component is a <a href="/docs/elements/web-components/form-association/">Form-Associated Custom Element</a>. It uses <code>ElementInternals</code> to:</p> <ul> <li>Submit the selected <code>value</code> via <code>FormData</code> (single mode: one value; multi mode: one entry per tag)</li> <li>Validate with <code>setValidity()</code> for required fields</li> <li>Reset on form reset via <code>formResetCallback()</code></li> <li>Restore state from browser history via <code>formStateRestoreCallback()</code> (single mode)</li> </ul> <p>No hidden inputs needed. The component name is set via the standard <code>name</code> attribute.</p> </section> <section> <h2>Styling</h2> <p>The component uses <code>@scope (combo-box)</code> for shared CSS encapsulation and <code>@scope (combo-box[multiple])</code> for tag-specific styles. Override option states and tag chip appearance using standard selectors:</p> <code-block language="css" show-lines label="Custom styling" data-escape>/* Option hover/active states */combo-box li[data-value]:hover { background: var(--color-surface-alt);} combo-box li[data-active] { outline: 2px solid var(--color-interactive);} /* Selected option */combo-box li[aria-selected="true"] { font-weight: 500; color: var(--color-interactive);} /* Multi-select tag chip styling */combo-box[multiple] .tag { background: var(--color-surface-raised); border: var(--border-width-thin) solid var(--color-border); border-radius: var(--radius-pill);} /* Remove button hover */combo-box[multiple] .tag button:hover { color: var(--color-error);}
Progressive Enhancement
Without JavaScript, <combo-box> renders as a plain text input above a visible, scrollable list. Users can still see all options and type into the input. Once JS loads, the component adds filtering, keyboard navigation, and form association.
/* Without JS: plain text input + visible list */combo-box:not(:defined) { display: block;} combo-box:not(:defined) > ul { list-style: none; padding: 0; margin-block-start: var(--size-xs); border: var(--border-width-thin) solid var(--color-border); border-radius: var(--radius-m); max-block-size: 12rem; overflow-y: auto;}
Related
- Form-Associated Custom Elements — Guide to the pattern
<star-rating>— Another form-associated component<drop-down>— Action menu (non-form)<datalist>— Native autocomplete