Welcome to Vanilla Breeze
This bell pulls live notifications from /go/notify/messages — the same contract documented at /docs/concepts/service-contracts/. Static articles like this one are the no-JS / no-backend fallback.
This bell pulls live notifications from /go/notify/messages — the same contract documented at /docs/concepts/service-contracts/. Static articles like this one are the no-JS / no-backend fallback.
Autocomplete combobox with filtering, keyboard navigation, and native form association. Supports single-select and multi-select tag modes.
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.
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>
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>
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>
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>
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>
| 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) |
| 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 |
| Attribute | On | Values | Description |
|---|---|---|---|
data-value | li | string | Option value identifier placed on each <li> in the options list |
| 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. |
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');});
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);});
| 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) |
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 = '';
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);}
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;}
<star-rating> — Another form-associated component<drop-down> — Action menu (non-form)<datalist> — Native autocomplete