Listbox

Listbox is a fully styled, accessible dropdown select. Use it when you need custom option rendering — icons, badges, multi-line text — that native <select> doesn't support. Built on HeadlessUI with full keyboard navigation and WCAG 2.2 AA compliance.


Installation

npm install @libretexts/davis-react

Basic Usage

import { Listbox } from '@libretexts/davis-react';
import { useState } from 'react';

const frameworks = ['React', 'Vue', 'Angular', 'Svelte', 'Solid'];

export default function Example() {
  const [value, setValue] = useState<string | null>(null);

  return (
    <Listbox value={value} onChange={setValue}>
      <Listbox.Label>Framework</Listbox.Label>
      <Listbox.Button placeholder="Select a framework…" />
      <Listbox.Options>
        {frameworks.map((f) => (
          <Listbox.Option key={f} value={f}>{f}</Listbox.Option>
        ))}
      </Listbox.Options>
    </Listbox>
  );
}

Object Values

Use the by prop to match objects by a specific key instead of by reference.

type Framework = { id: string; name: string; version: string };

const options: Framework[] = [
  { id: 'react', name: 'React', version: '19' },
  { id: 'vue', name: 'Vue', version: '3' },
  { id: 'angular', name: 'Angular', version: '18' },
];

const [selected, setSelected] = useState<Framework | null>(null);

<Listbox value={selected} onChange={setSelected} by="id">
  <Listbox.Label>Framework</Listbox.Label>
  <Listbox.Button
    displayValue={(f) => f ? `${f.name} v${f.version}` : ''}
    placeholder="Select a framework…"
  />
  <Listbox.Options>
    {options.map((f) => (
      <Listbox.Option key={f.id} value={f}>
        {f.name} <span className="text-gray-400 text-xs">v{f.version}</span>
      </Listbox.Option>
    ))}
  </Listbox.Options>
</Listbox>

Multiple Selection

Add multiple to allow selecting more than one value. The dropdown stays open so users can toggle selections.

const [selected, setSelected] = useState<string[]>([]);

<Listbox value={selected} onChange={setSelected} multiple>
  <Listbox.Label>Frameworks</Listbox.Label>
  <Listbox.Button
    displayValue={(v) => (v as string[]).length > 0 ? (v as string[]).join(', ') : ''}
    placeholder="Select frameworks…"
  />
  <Listbox.Options>
    {frameworks.map((f) => (
      <Listbox.Option key={f} value={f}>{f}</Listbox.Option>
    ))}
  </Listbox.Options>
</Listbox>

Disabled Options

Add disabled to individual options to prevent their selection.

<Listbox.Options>
  <Listbox.Option value="react">React</Listbox.Option>
  <Listbox.Option value="angular" disabled>Angular (unavailable)</Listbox.Option>
  <Listbox.Option value="vue">Vue</Listbox.Option>
</Listbox.Options>

Sizes

The size prop on Listbox.Button controls the button height and text size.

<Listbox value={value} onChange={setValue}>
  <Listbox.Label>Small</Listbox.Label>
  <Listbox.Button size="sm" placeholder="Select…" />
  <Listbox.Options>{/* ... */}</Listbox.Options>
</Listbox>

<Listbox value={value} onChange={setValue}>
  <Listbox.Label>Medium (default)</Listbox.Label>
  <Listbox.Button size="md" placeholder="Select…" />
  <Listbox.Options>{/* ... */}</Listbox.Options>
</Listbox>

<Listbox value={value} onChange={setValue}>
  <Listbox.Label>Large</Listbox.Label>
  <Listbox.Button size="lg" placeholder="Select…" />
  <Listbox.Options>{/* ... */}</Listbox.Options>
</Listbox>

With Label

Listbox.Label links a visible label to the listbox for screen readers.

<Listbox value={value} onChange={setValue}>
  <Listbox.Label>Preferred framework</Listbox.Label>
  <Listbox.Button placeholder="Select…" />
  <Listbox.Options>{/* ... */}</Listbox.Options>
</Listbox>

Props

Listbox

PropTypeDefaultDescription
valueT | nullrequiredCurrently selected value
onChange(value: T) => voidrequiredCalled when selection changes
disabledbooleanfalseDisables the entire listbox
multiplebooleanfalseAllows selecting multiple values (value becomes T[])
namestringHTML form field name for native form submission
bykeyof T | (a: T, b: T) => booleanKey or function to compare option values
classNamestringAdditional CSS classes on the wrapper

Listbox.Label

PropTypeDefaultDescription
classNamestringAdditional CSS classes

Listbox.Button

PropTypeDefaultDescription
displayValue(value: T | null) => stringString(v)Converts selected value to display string
placeholderstring"Select…"Text shown when no value is selected
size'sm' | 'md' | 'lg''md'Controls button height and text size
disabledbooleanfalseDisables the button
aria-labelstringAccessible label when no visible Listbox.Label is used
classNamestringAdditional CSS classes

Listbox.Options

PropTypeDefaultDescription
classNamestringAdditional CSS classes

Listbox.Option

PropTypeDefaultDescription
valueTrequiredThe option value, passed to onChange on selection
disabledbooleanfalsePrevents selection and skips in keyboard navigation
classNamestringAdditional CSS classes

Accessibility

Listbox implements the ARIA listbox pattern. HeadlessUI manages all ARIA attributes automatically.

Keyboard navigation:

KeyAction
/ Navigate through options
Enter / SpaceSelect the focused option
EscapeClose the dropdown
TabMove focus out of the listbox
Home / EndJump to first / last option

Best practices:

  • Always provide either Listbox.Label or an aria-label on Listbox.Button so screen readers can identify the control.
  • When using multiple, consider displaying a count or list of selected values in the button so users can see their selections without opening the dropdown.

When to use

  • Select — Use for 5–15 static options in a simple form. Native browser styling, maximum compatibility, no JavaScript required.
  • Listbox — Use when you need custom option rendering (icons, badges, multi-line text, custom layout), or when the native <select> appearance doesn't match your design system. Functionally identical to <select> from an accessibility standpoint.
  • Combobox — Use when the list has many options (20+) and filtering by typing would help users find what they need faster.