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>
<% %w[apple apricot banana cherry].each do |fruit| %>
<% label = t("components.combobox.demo.fruits.#{fruit}") %>
<li role="option" id="fruit-<%= fruit %>" class="combobox__option" data-value="<%= label %>"
data-stimeo--combobox-target="option"
data-action="click->stimeo--combobox#select"><%= label %></li>
<% end %>
</ul>
<%# The no-results message is a region of the empty state: the library shows it while the
open listbox has no match and hides it otherwise, so it is authored hidden. Copy via
i18n. %>
<div
class="combobox__empty"
data-stimeo--combobox-target="empty"
hidden><%= t("components.combobox.demo.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);
}
/*
* The no-results message from the ERB (copy lives in the locale files). The library owns
* its hidden attribute and shows it only while there are no matches, so what this file
* gives it is the surface, not its visibility.
*/
.combobox__empty {
position: absolute;
left: 0;
right: 0;
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);
}
/*
* While there are no matches the library sets data-stimeo--combobox-empty on the root.
* Dropping the list is this demo's own choice: the message takes its place instead of
* sitting under an empty popup.
*/
.combobox[data-stimeo--combobox-empty] .combobox__list {
display: none;
}
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" |
empty
|
A region shown only while the open list has no visible option; the controller owns its hidden. Place it outside the list target, whose children are options. |
data-stimeo--combobox-target="empty" |
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 |
select
|
Selects an owned option from an event or element, fills the input and closes the listbox. | stimeo--combobox#select |
Events
| Name | Description | Event |
|---|---|---|
selected
|
Fires when an option is committed; detail { value, reason } (the option's data-value or text). detail.reason reports user / focus / pointer / api. |
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. |