Skip to content

Find what you need, instantly

Search components, ARIA roles & attributes, and WCAG criteria.

Forms & ControlsFlagship guide

Combobox

A text input combined with a popup listbox that filters/suggests options as the user types.

Last verified against WCAG 2.2 and WAI-ARIA APG 1.2 on 2026-07-02.

Implementation

combobox-pattern.tsx
function ComboboxPattern() {
  const [value, setValue] = useState("");
  const [open, setOpen] = useState(false);
  const [activeIndex, setActiveIndex] = useState(-1);
  const inputRef = useRef(null);

  const options = FRUITS.filter(f =>
    f.toLowerCase().includes(value.toLowerCase())
  );

  function selectOption(option) {
    setValue(option);
    setOpen(false);
    setActiveIndex(-1);
    inputRef.current?.focus(); // focus never leaves the input
  }

  function onKeyDown(e) {
    if (e.key === "ArrowDown") {
      e.preventDefault();
      setOpen(true);
      setActiveIndex(i => Math.min(i + 1, options.length - 1));
    } else if (e.key === "ArrowUp") {
      e.preventDefault();
      setActiveIndex(i => Math.max(i - 1, 0));
    } else if (e.key === "Enter" && open && activeIndex >= 0) {
      e.preventDefault();
      selectOption(options[activeIndex]);
    } else if (e.key === "Escape" && open) {
      e.preventDefault();
      setOpen(false);
    }
  }

  return (
    <>
      <input
        ref={inputRef}
        role="combobox"
        aria-expanded={open}
        aria-controls="fruit-listbox"
        aria-autocomplete="list"
        // Points at the visually-highlighted option WITHOUT moving real
        // DOM focus — this is what lets the input keep the caret & keeps
        // the screen reader in "edit text" mode while announcing options.
        aria-activedescendant={
          activeIndex >= 0 ? `fruit-option-${activeIndex}` : undefined
        }
        value={value}
        onChange={(e) => { setValue(e.target.value); setOpen(true); }}
        onKeyDown={onKeyDown}
      />
      {open && (
        <ul id="fruit-listbox" role="listbox" aria-label="Fruits">
          {options.map((option, i) => (
            <li
              key={option}
              id={`fruit-option-${i}`}
              role="option"
              aria-selected={i === activeIndex}
              onMouseDown={(e) => { e.preventDefault(); selectOption(option); }}
            >
              {option}
            </li>
          ))}
        </ul>
      )}
    </>
  );
}

Live demo

Required roles, states & properties

Required roles, states, and properties
ElementAttributeWhy
Text inputrole="combobox"Identifies the input as a combobox, not a plain textbox, so AT announces "combobox" and exposes expand/collapse state.
Text inputaria-expandedTells AT whether the suggestion popup is currently open — announced as "collapsed" or "expanded."
Text inputaria-controlsAssociates the input with the popup listbox it controls, by id.
Text inputaria-autocomplete="list"Tells AT that typing produces a filtered list of suggestions (as opposed to inline text completion).
Text inputaria-activedescendantPoints at the id of the currently-highlighted option while DOM focus stays on the input, so the caret and screen reader cursor never leave the edit field.
Popup listrole="listbox"Identifies the popup as a list of selectable options.
Each optionrole="option" + aria-selectedIdentifies each row as selectable and communicates which one is currently active.

Keyboard interaction model

Keyboard interaction model
KeyBehavior
Down ArrowOpens the popup if closed; otherwise moves the active option to the next item.
Up ArrowMoves the active option to the previous item.
EnterCommits the active option's value into the input and closes the popup.
EscapeCloses the popup without changing the input value.
Home / EndMoves the active option to the first / last item in the popup.
Printable charactersFilters the option list; popup opens automatically.

Focus management rules

  • DOM focus stays on the text input at all times — never moves into the popup.
  • The active option is communicated via aria-activedescendant, not real focus.
  • Selecting an option (mouse or keyboard) returns focus to the input immediately.
  • Closing the popup (Escape or outside click) never moves focus away from the input.

WCAG 2.2 success criteria mapping

WCAG 2.2 success criteria that apply to this component
SCNameLevelWhy it applies
1.3.1Info and RelationshipsAStructure and relationships conveyed visually are also programmatically determinable.
2.1.1KeyboardAAll functionality is operable through a keyboard interface with no specific timing.
2.4.3Focus OrderAFocusable components receive focus in an order that preserves meaning and operability.
3.3.2Labels or InstructionsALabels or instructions are provided when content requires user input.
4.1.2Name, Role, ValueAFor all UI components, name, role, and value are programmatically determinable; states and changes are announced.

References