Combobox
stimeo--combobox
A text field whose choices narrow as you type, and that you can drive from the keyboard.
Implements the WAI-ARIA Combobox pattern. Typing narrows the choices and drops the ones that do not match. While the arrow keys move through them, focus stays in the field so you can keep typing, and screen readers are told which choice is current. Picking one fills the field and closes the list.
- Apple
- Apricot
- Banana
- Cherry
Keyboard
| Key | Action |
|---|---|
| ↓ / ↑ | Move the active option (wrapping). |
| Enter | Select the active option and close the list. |
| Home / End | Activate the first / last visible option. |
| Esc | Close the listbox. |
<%# Markup for the combobox (APG Combobox / list autocomplete) demo.
stimeo--combobox provides list filtering on input, Arrow/Enter/Esc, and
aria-activedescendant. Focus stays on the input and the active option is shown via
aria-selected. On selection, data-value (or the text, if absent) is placed in the input. %>
<div class="combobox" data-controller="stimeo--combobox">
<label id="fruit-label" class="combobox__label" for="fruit-input"><%= t(
"components.combobox.demo.label"
) %></label>
<input
id="fruit-input"
type="text"
class="combobox__input"
role="combobox"
aria-expanded="false"
aria-autocomplete="list"
aria-controls="fruit-list"
autocomplete="off"
data-stimeo--combobox-target="input"
data-action="input->stimeo--combobox#filter
keydown->stimeo--combobox#onKeydown
focus->stimeo--combobox#open
click->stimeo--combobox#open">
<ul
id="fruit-list"
class="combobox__list"
role="listbox"
aria-labelledby="fruit-label"
data-stimeo--combobox-target="list"
hidden>
<li role="option" id="fruit-apple" class="combobox__option" data-value="Apple"
data-stimeo--combobox-target="option"
data-action="click->stimeo--combobox#selectByClick">Apple</li>
<li role="option" id="fruit-apricot" class="combobox__option" data-value="Apricot"
data-stimeo--combobox-target="option"
data-action="click->stimeo--combobox#selectByClick">Apricot</li>
<li role="option" id="fruit-banana" class="combobox__option" data-value="Banana"
data-stimeo--combobox-target="option"
data-action="click->stimeo--combobox#selectByClick">Banana</li>
<li role="option" id="fruit-cherry" class="combobox__option" data-value="Cherry"
data-stimeo--combobox-target="option"
data-action="click->stimeo--combobox#selectByClick">Cherry</li>
</ul>
<%# Empty state shown when there are no matches. CSS reacts to
data-stimeo--combobox-empty (set on the root by the library) to reveal it. Copy via i18n. %>
<div class="combobox__empty"><%= t("components.combobox.empty") %></div>
</div>
/*
* Presentation-only styles for the combobox demo.
* The active option is highlighted via aria-selected (set by the library).
*/
.combobox {
position: relative;
display: inline-block;
width: min(20rem, 100%);
}
.combobox__label {
display: block;
margin-bottom: 0.25rem;
font-size: 0.875rem;
color: var(--muted);
}
.combobox__input {
width: 100%;
padding: 0.5rem 0.75rem;
font-size: 1rem;
border: 1px solid var(--border-interactive);
border-radius: 0.375rem;
}
.combobox__list {
position: absolute;
left: 0;
right: 0;
margin: 0.25rem 0 0;
padding: 0.25rem;
list-style: none;
max-height: 12rem;
overflow-y: auto;
background: var(--surface-card);
border: 1px solid var(--border-strong);
border-radius: 0.375rem;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
}
.combobox__option {
padding: 0.5rem 0.75rem;
border-radius: 0.25rem;
cursor: pointer;
}
.combobox__option[aria-selected="true"] {
background: var(--accent);
color: var(--white);
}
/*
* When there are no matches, the library sets data-stimeo--combobox-empty on the root.
* Rather than showing an empty list, this is an example of consumer CSS reacting to it
* to show the i18n'd .combobox__empty from the ERB (copy lives in the locale files).
*/
.combobox__empty {
position: absolute;
left: 0;
right: 0;
display: none;
margin: 0.25rem 0 0;
padding: 0.5rem 0.75rem;
background: var(--surface-card);
border: 1px solid var(--border-strong);
border-radius: 0.375rem;
color: var(--muted);
font-size: 0.875rem;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
}
.combobox[data-stimeo--combobox-empty] .combobox__list {
display: none;
}
.combobox[data-stimeo--combobox-empty] .combobox__empty {
display: block;
}
This demo needs no consumer-side JS (the controller handles the behavior).
These demo styles use shared design tokens (light + dark). Copy the shared styles too, then toggle data-theme on your root element for dark mode.
The data-* attributes you add to your own HTML to wire this component. Put the data-controller below on a root element, then place its targets / values / actions inside that element.
On the root element
data-controller="stimeo--combobox"
Targets
| Name | Description | Attribute |
|---|---|---|
input
required
|
The combobox text input that drives filtering and tracks the active option via aria-activedescendant. |
data-stimeo--combobox-target="input" |
list
required
|
The role=listbox popup, toggled via its hidden attribute. |
data-stimeo--combobox-target="list" |
option
|
An authored role=option; the controller filters it by toggling hidden and marks the active one. |
data-stimeo--combobox-target="option" |
Actions
| Name | Description | Action |
|---|---|---|
close
|
Hides the listbox, clears the active option, and updates ARIA state. | stimeo--combobox#close |
filter
|
Filters options against the input value and opens the listbox. | stimeo--combobox#filter |
onKeydown
|
Handles keyboard interaction per the APG combobox model (ArrowUp/Down wrapping, Home/End, Enter, Escape, Tab). | stimeo--combobox#onKeydown |
open
|
Opens the listbox, re-filtering against the current input value. | stimeo--combobox#open |
selectByClick
|
Selects the clicked option, filling the input and closing the listbox. | stimeo--combobox#selectByClick |
Events
| Name | Description | Event |
|---|---|---|
selected
|
Fires when an option is committed; detail { value } (the option's data-value or text). |
stimeo--combobox:selected |
State hooks
The library only manages these ARIA/data attributes and custom properties. Your CSS reads them to render the look — selectors like [aria-selected], [aria-expanded], or var(--stimeo--…) hook into this state.
| Hook | Target | Meaning |
|---|---|---|
aria-expanded |
Input | "true" while the listbox is open, "false" when closed. |
aria-selected |
Active option | "true" on the option the keyboard is currently on. It marks where you are, not what you chose — the committed value goes into the input. The controller owns it; do not author it. |
aria-activedescendant |
Input | Holds the id of the active option for assistive technology. |
hidden |
Options | Applied to options filtered out by the typed query. |
data-stimeo--combobox-empty |
Root element | Present when the listbox is open but no option matches. |