Combobox
stimeo--combobox
A text input with a filtered listbox popup (list autocomplete) and full keyboard support.
The stimeo--combobox controller implements the WAI-ARIA Combobox pattern with list autocomplete. Typing filters the DOM options by hiding non-matching entries; focus stays in the input and the active option is tracked with aria-activedescendant. Selecting an option fills the input (with its data-value if set) and closes the listbox. The library provides behavior only — styling is owned by this Playground.
- 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 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"
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;
* SKILL: playground-i18n).
*/
.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 currently highlighted option. |
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. |