Loading Developer Playground

Loading ...

Skip to main content
ARIA ROLEComposite RolesEditable widgetPopup controllerAutocomplete ready

combobox

An editable text input that controls a popup with suggested options. A combobox pairs free-form typing with a listbox, grid, or tree so people can find and select values efficiently.

Live Example

Searchable Combobox

Editable input with suggestion popup, arrow navigation, and Enter to confirm selection.

Use arrow keys to explore suggestions and Enter to select.

  • React
  • Vue
  • Svelte
  • Solid
  • Angular
  • Ember
  • Preact

Purpose

Let users type or pick a value from a controlled popup while keeping focus on a single text field.

Supported Popups

Can control listbox, grid, or tree widgets via aria-haspopup coupled with aria-controls.

Screen Reader

Announced as “combobox collapsed/expanded”. Users expect arrow keys to open and traverse results.

When to Use

1

Search + Autocomplete

Offer live search suggestions for people typing queries, destinations, or commands.

2

Typeahead Selection

Let users type partial values (countries, tags, products) and confirm with Enter.

3

Filter Large Lists

Expose thousands of records without forcing people to scroll through long select menus.

Required Structure

Labeled Text Input

Expose an input with role="combobox" (or input[type="text"] + role) that has an accessible name and optional inline help.

Popup Container

Connect the combobox to a role="listbox" | "grid" | "tree" element via aria-controls. Keep the popup in the DOM so assistive tech can discover options.

Active Option Tracking

Manage aria-activedescendant on the input (for true comboboxes) or move DOM focus inside the popup. Only one option should be active at a time.

Keyboard Interaction Model

ActionKeysResult
Open popupArrowDown / Alt + ArrowDown / EnterOpens the popup and moves focus/active item to the first selectable option.
Navigate optionsArrowUp / ArrowDownMoves the active option without leaving the input. Update aria-activedescendant accordingly.
Jump to edgesHome / EndMoves to the first or last option within the popup.
Commit selectionEnter / TabConfirms the currently active option and closes the popup.
Dismiss popupEscapeCloses the popup and restores the previous input value if needed.
TypeaheadPrintable charactersFilters the popup options and updates aria-busy if you debounce results.

Required States & Properties

aria-controlsRequired

References the popup element that contains the available options.

aria-expandedRequired

Reflects whether the popup is currently visible. Toggle between true and false when opening.

aria-haspopupRequired

Communicates the popup type (listbox, grid, tree). Screen readers adjust commands accordingly.

aria-activedescendantOptional

Points to the id of the option that is currently highlighted while focus stays on the input.

aria-autocompleteOptional

Use "list", "both", or "inline" to describe how suggestions behave as the user types.

aria-requiredOptional

Mark the combobox required and announce validation feedback via aria-invalid and aria-describedby.

aria-disabledOptional

When true, prevents expansion and selection. Always pair with a disabled visual state.

Implementation Checklist

  • Keep focus on the input element. Drive option highlighting with aria-activedescendant so screen readers stay anchored.

  • Maintain DOM ownership of the popup even when hidden (use CSS to hide). Removing it breaks assistive tech references.

  • Constrain popup width to match the input or ensure offset is announced via aria-owns/aria-controls if detached.

  • If results load asynchronously, set aria-busy on the popup region and provide polite status text.

  • Ensure pointer interactions mirror keyboard behavior (hover should not steal focus without keyboard confirmation).

Code Examples

Combobox with Listbox Popup

Input keeps focus while aria-activedescendant follows the highlighted option.

<label for="country-combobox" class="sr-only">Country</label>
<div class="combobox">
  <input
    id="country-combobox"
    type="text"
    role="combobox"
    aria-haspopup="listbox"
    aria-expanded="false"
    aria-controls="country-listbox"
    aria-autocomplete="list"
    autocomplete="off"
  />

  <ul id="country-listbox" role="listbox" class="hidden">
    <li id="country-option-1" role="option">Canada</li>
    <li id="country-option-2" role="option">Germany</li>
    <li id="country-option-3" role="option">Japan</li>
  </ul>
</div>

React Combobox with aria-activedescendant

State-driven popup that supports keyboard filtering and commits a selected option.

import { useMemo, useState } from 'react'

const countries = ['Canada', 'Germany', 'Japan', 'Kenya', 'Peru']

export function CountryCombobox() {
  const [value, setValue] = useState('')
  const [expanded, setExpanded] = useState(false)
  const [activeIndex, setActiveIndex] = useState<number | null>(null)

  const filtered = useMemo(
    () => countries.filter((item) => item.toLowerCase().includes(value.toLowerCase())),
    [value]
  )

  const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
    if (!expanded && ['ArrowDown', 'ArrowUp'].includes(event.key)) {
      event.preventDefault()
      setExpanded(true)
      setActiveIndex(0)
      return
    }

    if (event.key === 'ArrowDown') {
      event.preventDefault()
      setActiveIndex((prev) => Math.min((prev ?? -1) + 1, filtered.length - 1))
    } else if (event.key === 'ArrowUp') {
      event.preventDefault()
      setActiveIndex((prev) => Math.max((prev ?? filtered.length) - 1, 0))
    } else if (event.key === 'Enter' && activeIndex !== null) {
      event.preventDefault()
      setValue(filtered[activeIndex])
      setExpanded(false)
    } else if (event.key === 'Escape') {
      setExpanded(false)
      setActiveIndex(null)
    }
  }

  const activeId = activeIndex === null ? undefined : `country-${activeIndex}`

  return (
    <div className="relative">
      <label htmlFor="country" className="sr-only">
        Country
      </label>
      <input
        id="country"
        role="combobox"
        aria-expanded={expanded}
        aria-controls="country-listbox"
        aria-haspopup="listbox"
        aria-activedescendant={activeId}
        value={value}
        onChange={(event) => {
          setValue(event.target.value)
          setExpanded(true)
        }}
        onKeyDown={handleKeyDown}
        onBlur={() => setExpanded(false)}
      />

      <ul
        id="country-listbox"
        role="listbox"
        className={expanded ? 'absolute top-full left-0 w-full bg-white shadow-lg' : 'hidden'}
      >
        {filtered.map((item, index) => (
          <li
            id={`country-${index}`}
            role="option"
            key={item}
            aria-selected={index === activeIndex}
            onMouseDown={() => {
              setValue(item)
              setExpanded(false)
            }}
          >
            {item}
          </li>
        ))}
      </ul>
    </div>
  )
}

Best Practices

🪄

Keep focus stable

Never move DOM focus into the popup. Use aria-activedescendant so assistive tech stays in the input.

📝

Explain shortcuts

Provide inline helper text that documents arrow keys, Enter to select, and Escape to close.

📉

Limit result count

Do not flood the popup with hundreds of options. Paginate or virtualize while updating aria-rowcount.

🔄

Sync value + option

Keep the text field in sync with the confirmed option to avoid confusion for screen reader users.

Common Mistakes

Missing aria-controls link

The input does not reference the popup element. Screen readers cannot reach the options.

Set aria-controls to the id of the listbox/grid/tree that contains the suggestions.

Incorrect
<input role="combobox" aria-expanded="true">
<ul role="listbox" id="results"></ul>
Correct
<input role="combobox" aria-expanded="true" aria-controls="results">
<ul role="listbox" id="results"></ul>

Moving focus into popup

Focus jumps from the input into the list of options, so typing no longer updates suggestions.

Keep focus on the combobox and drive highlighting with aria-activedescendant.

Related Roles & Patterns

listbox

Default popup type that contains selectable option elements.

Learn more

option

Individual option that appears inside a listbox or tree.

Learn more

textbox

Native input element that a combobox enhances with suggestions.

Learn more

grid

Structured popup used for spreadsheet-like comboboxes.

Learn more