Skip to content

Listbox ​

Overview ​

Single and multi-selection with active-descendant navigation.

Pattern: Listbox. Status: stable.

When to use ​

  • Single or multiple option selection with active-descendant navigation.
  • Virtual or custom-rendered lists where the input focus should remain on the listbox container.

When not to use ​

  • Editable autocomplete; use Combobox.
  • Menu actions that do not represent selected values.

Import ​

ts
import { createListbox } from 'ui-headless-runtime';

Controller creation ​

Create Listbox during component mount or setup, subscribe before rendering derived UI, and keep every cleanup returned by registrations or DOM binding.

Options ​

  • Public options: id, loop, selectionMode, defaultValue, getValue, onValueChange, subscribeValue.
  • Disabled state, display text, IDs, and optional submitted values belong to registered ListboxOption objects.

Snapshot ​

  • Snapshot fields: activeId, ariaMultiselectable, controlled, id, options, role, selectedValues.
  • Each option snapshot contains id, text, value, disabled, selected, and role.

Commands ​

  • Component commands: handleKeyDown, registerOption, select, setActive.
  • Multiple-mode toggling is requested through select; there is no separate public toggle command.

Events ​

  • Events: beforeSelect, select, stateChange.
  • Payloads contain the selected option, resulting selectedValues, and typed change details.

Change reasons ​

  • Change reasons: programmatic, pointer, keyboard, typeahead.

Controlled mode ​

Controlled listboxes emit a requested readonly string[]; single-select mode limits that array to at most one value. The controller waits for the consumer store to commit it.

Uncontrolled mode ​

Uncontrolled listboxes own selected value(s) and active option.

DOM binding ​

  • Register options as they render; apply active-descendant metadata to the focusable listbox element.

Required markup ​

  • Use role="listbox" and role="option" with visible option text.

ARIA contract ​

  • Apply aria-selected, aria-disabled, aria-activedescendant, and aria-multiselectable from the snapshot.

Keyboard interaction ​

  • ArrowDown / ArrowUp: Move the active item, skipping disabled items.
  • Home / End: Move to the first or last enabled item.
  • Enter / Space: Select the active option.
  • Type characters: Move by normalized typeahead.

Focus behavior ​

  • Focus remains on the listbox container while Arrow keys update the active option.

Nested behavior ​

  • Listboxes are not overlay managers; pair with Popover only when you need anchored display.

Cleanup ​

  • Dynamic option removal recalculates active item and selected values.

Minimal lifecycle example ​

ts
import { createListbox } from 'ui-headless-runtime';

const controller = createListbox();
const unsubscribe = controller.subscribe((snapshot) => {
  console.log(snapshot);
});

console.log(controller.getSnapshot());
unsubscribe();
controller.destroy();

The production demo loads the exact executable module from apps/demo/src/examples/listbox.ts.

Edge cases ​

  • single: One selected value.
  • multiple: Selection toggles independently.
  • disabled: Unavailable options are skipped.

Limitations ​

  • Filtering and typed input are Combobox responsibilities.

API reference ​

See createListbox.

Headless behavior; consumer-owned rendering and styling.