Combobox
ui-combobox is a text input with a filterable list of choices, following the ARIA combobox pattern. The choices
are plain <option> elements written as children, like in a <select>. Typing filters them by label, and picking
one sets the element's value. The list is a native manual popover, so it renders in the top layer. It opens when
the input is focused or clicked, and closes on Escape, on a choice and when focus leaves the input.
It is form-associated. With a name, the chosen option's value is submitted with the form. Like a native form
control in a form field, it fills the width of its container. width gives it a fixed width instead.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Combobox demo</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/dist/theme.css">
<script type="module" src="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js"></script>
</head>
<body>
<import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/combobox.html"></import-element>
<ui-combobox aria-label="Framework" placeholder="Select a framework…" width="60">
<option value="realm">REALM</option>
<option value="lit">Lit</option>
<option value="stencil">Stencil</option>
<option value="fast">FAST</option>
<option value="vanilla">Vanilla custom elements</option>
</ui-combobox>
</body>
</html>Examples
Preselected value
Set value to the value of one of the options. The input shows that option's label.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Combobox demo</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/dist/theme.css">
<script type="module" src="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js"></script>
</head>
<body>
<import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/combobox.html"></import-element>
<ui-combobox aria-label="Time zone" value="europe-berlin">
<option value="america-new-york">New York (UTC-5)</option>
<option value="europe-london">London (UTC+0)</option>
<option value="europe-berlin">Berlin (UTC+1)</option>
<option value="asia-tokyo">Tokyo (UTC+9)</option>
</ui-combobox>
</body>
</html>Disabled options
A disabled option is shown dimmed and cannot be chosen.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Combobox demo</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/dist/theme.css">
<script type="module" src="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js"></script>
</head>
<body>
<import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/combobox.html"></import-element>
<ui-combobox aria-label="Plan" placeholder="Choose a plan…">
<option value="free">Free</option>
<option value="pro">Pro</option>
<option value="enterprise" disabled>Enterprise (contact sales)</option>
</ui-combobox>
</body>
</html>Option labels
The visible text comes from the option's label attribute, or its text when there is no label, as with native
options. Filtering matches the label.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Combobox demo</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/dist/theme.css">
<script type="module" src="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js"></script>
</head>
<body>
<import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/combobox.html"></import-element>
<ui-combobox aria-label="Country" placeholder="Search countries…">
<option value="DE" label="Germany">DE</option>
<option value="FR" label="France">FR</option>
<option value="JP" label="Japan">JP</option>
<option value="BR" label="Brazil">BR</option>
</ui-combobox>
</body>
</html>Empty state
ui-combobox-empty replaces the default "No results found." text shown when nothing matches the query.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Combobox demo</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/dist/theme.css">
<script type="module" src="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js"></script>
</head>
<body>
<import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/combobox.html"></import-element>
<ui-combobox aria-label="Language" width="60">
<option value="en">English</option>
<option value="es">Spanish</option>
<option value="id">Indonesian</option>
<ui-combobox-empty>No language matches that search.</ui-combobox-empty>
</ui-combobox>
</body>
</html>Width
By default the combobox fills its container. width sets a fixed width in spacing units, so width="48" is
calc(var(--spacing) * 48).
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Combobox demo</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/dist/theme.css">
<script type="module" src="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js"></script>
<style>
.demo-stack {
display: grid;
gap: calc(var(--spacing) * 4);
}
</style>
</head>
<body>
<import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/combobox.html"></import-element>
<div class="demo-stack">
<ui-combobox aria-label="Fruit" placeholder="Fills the container">
<option value="apple">Apple</option>
</ui-combobox>
<ui-combobox aria-label="Fruit" placeholder="width=48" width="48">
<option value="apple">Apple</option>
</ui-combobox>
</div>
</body>
</html>Disabled
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Combobox demo</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/dist/theme.css">
<script type="module" src="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js"></script>
</head>
<body>
<import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/combobox.html"></import-element>
<ui-combobox aria-label="Framework" value="realm" disabled>
<option value="realm">REALM</option>
</ui-combobox>
</body>
</html>Dynamic options
The combobox watches its <option> children, so options rendered from state with repeat show up in the list.
value:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Combobox demo</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/dist/theme.css">
<script type="module" src="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js"></script>
</head>
<body>
<import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/button.html"></import-element>
<import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/combobox.html"></import-element>
<import-element from="./combobox-dynamic-demo.html"></import-element>
<combobox-dynamic-demo></combobox-dynamic-demo>
</body>
</html><custom-element name="combobox-dynamic-demo">
<script>
const catalog = useState({
fruits: [
{ id: "apple", name: "Apple" },
{ id: "banana", name: "Banana" },
],
chosen: "",
});
function addCherry() {
if (catalog.fruits.some((fruit) => fruit.id === "cherry")) return;
catalog.fruits = [...catalog.fruits, { id: "cherry", name: "Cherry" }];
}
function changed(combobox) {
catalog.chosen = combobox.value;
}
</script>
<style>
:host {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: calc(var(--spacing) * 3);
}
p {
margin: 0;
font-size: var(--text-sm);
color: var(--muted-foreground);
}
</style>
<ui-combobox aria-label="Fruit" placeholder="Pick a fruit…" onchange="changed(this)">
<option repeat="#catalog.fruits" as="fruit" key="id" _value="#fruit.id" _label="#fruit.name"></option>
</ui-combobox>
<ui-button variant="outline" onclick="addCherry()">Add cherry</ui-button>
<p>value: <slot name="#catalog.chosen"></slot></p>
</custom-element>In a form
The chosen value is submitted under the element's name. With required, the form is invalid until an option is
chosen, and form.reset() returns to the value attribute.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Combobox demo</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/dist/theme.css">
<script type="module" src="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js"></script>
</head>
<body>
<import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/button.html"></import-element>
<import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/combobox.html"></import-element>
<import-element from="./combobox-form-demo.html"></import-element>
<combobox-form-demo></combobox-form-demo>
</body>
</html><custom-element name="combobox-form-demo">
<script>
const result = useState({ text: "Submit the form to see its data." });
function submit(event) {
event.preventDefault();
const entries = [...new FormData(event.target)].map(([name, value]) => `${name}=${value}`);
result.text = entries.join("\n") || "(nothing submitted)";
}
function reset() {
result.text = "The form was reset.";
}
</script>
<style>
:host {
display: grid;
gap: calc(var(--spacing) * 4);
width: 100%;
max-width: calc(var(--spacing) * 80);
}
form {
display: grid;
gap: calc(var(--spacing) * 3);
}
.actions {
display: flex;
gap: calc(var(--spacing) * 2);
}
output {
display: block;
padding: calc(var(--spacing) * 3);
border-radius: var(--radius-md);
background: var(--muted);
font-family: var(--font-mono);
font-size: var(--text-xs);
white-space: pre-wrap;
}
</style>
<form onsubmit="submit(event)" onreset="reset()">
<ui-combobox name="framework" aria-label="Framework" required>
<option value="realm">REALM</option>
<option value="lit">Lit</option>
<option value="stencil">Stencil</option>
</ui-combobox>
<div class="actions">
<ui-button type="submit">Save</ui-button>
<ui-button type="reset" variant="outline">Reset</ui-button>
</div>
</form>
<output><slot name="#result.text"></slot></output>
</custom-element>API
Parts
| Part | Slot | Renders |
|---|---|---|
ui-combobox |
The text input with a chevron and the popover list. Fills its container's width unless width is set. |
|
<option> |
(data) | One choice in the list, with a check mark when chosen. |
ui-combobox-empty |
empty |
The text shown when nothing matches, in place of "No results found.". |
Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
value |
string | "" |
The value of the chosen option. |
placeholder |
string | "Select…" |
Placeholder of the text input. |
width |
number | Fixed width in spacing units, where n means calc(var(--spacing) * n). Without it the combobox fills its container. |
|
disabled |
boolean | false |
Disables the input. Also true inside a disabled <fieldset>. |
required |
boolean | false |
The form is invalid until an option is chosen. |
aria-label |
string | Accessible name, passed to the inner input. Property name: label. |
Properties
| Property | Type | Description |
|---|---|---|
value |
string | The chosen value. Setting it selects the matching option. |
Events
| Event | Detail | Description |
|---|---|---|
change |
none | Fired after the user chooses an option. |
Child elements
| Element | Attributes read | Description |
|---|---|---|
<option> |
value, label (or text), disabled |
One choice. Options are hidden in place and rendered in the list. Adding, removing or changing options updates the list. |
ui-combobox-empty |
Rich content for the empty state. |
CSS parts
| Part | Description |
|---|---|
field |
The bordered box around the input and chevron. |
input |
The inner text <input role="combobox">. |
list |
The popover role="listbox" element. |
option |
Each rendered option (role="option"). |
empty |
The paragraph shown when nothing matches. |
Forms
ui-combobox is form-associated. With a name, it submits value, or an empty string when nothing is chosen. With
required and no value it reports valueMissing with the message "Please select an item." and the validation
bubble points at the input. form.reset() restores the value attribute.
Accessibility
The inner input has role="combobox", aria-autocomplete="list", aria-controls pointing at the listbox and
aria-expanded while the list is open. Options have role="option", with aria-selected on the chosen one and
aria-disabled on disabled ones. The highlighted option follows the arrow keys while focus stays in the input, and
the input's aria-activedescendant points at it while the list is open.
| Key | Action |
|---|---|
| Tab into the input | Opens the list. |
| Typing | Opens the list and filters the options by label. |
| Arrow Down / Arrow Up | Opens the list and moves the highlight, wrapping at the ends. |
| Enter | Chooses the highlighted option and closes the list. |
| Escape | Closes the list. |