Skip to content

Early previewhamur is not on npm yet. The components are being built in the open, follow along on GitHub.

InputsReact

Listbox

An always visible list of options where one value is selected, or several with multiple.

Rendered by React 19.3.1

Loading React…
import { Listbox } from '@hamur/react/listbox'

const fruits = [
  { value: 'apple', label: 'Apple' },
  { value: 'banana', label: 'Banana' },
  { value: 'blueberry', label: 'Blueberry' },
  { value: 'grape', label: 'Grape', disabled: true },
  { value: 'mango', label: 'Mango' },
  { value: 'pineapple', label: 'Pineapple' },
]

export default function ListboxExample() {
  return (
    <div className="field">
      <div id="listbox-fruit-label" className="listbox-label">
        Favorite fruit
      </div>
      <Listbox.Root
        className="listbox"
        defaultValue="banana"
        aria-labelledby="listbox-fruit-label"
      >
        {fruits.map((fruit) => (
          <Listbox.Item
            key={fruit.value}
            value={fruit.value}
            disabled={fruit.disabled}
            className="listbox-item"
          >
            <Listbox.ItemIndicator className="listbox-item-indicator">
              ✓
            </Listbox.ItemIndicator>
            <Listbox.ItemText className="listbox-item-text">
              {fruit.label}
            </Listbox.ItemText>
          </Listbox.Item>
        ))}
      </Listbox.Root>
    </div>
  )
}
Not measured yetTests passing in React
Not measured yetSize
Not measured yetMemory
Not measured yetSpeed
[N] commits

Installation

npm i @hamur/react

Not on npm yet. The package goes out under the chosen name.

Anatomy

Import the component and assemble its parts:

<Listbox.Root>
  <Listbox.Item>
    <Listbox.ItemIndicator />
    <Listbox.ItemText />
  </Listbox.Item>
</Listbox.Root>

// Not in this example: Listbox.Empty, Listbox.Group, Listbox.GroupLabel, Listbox.Separator

Examples

Multiple

multiple keeps several values; the selected toppings are listed below.

ListboxMultiple.tsx
import * as React from 'react'
import { Listbox } from '@hamur/react/listbox'

const toppings = [
  { value: 'cheese', label: 'Cheese' },
  { value: 'mushrooms', label: 'Mushrooms' },
  { value: 'olives', label: 'Olives' },
  { value: 'onions', label: 'Onions' },
  { value: 'peppers', label: 'Peppers' },
  { value: 'tomatoes', label: 'Tomatoes' },
]

export default function ListboxMultipleExample() {
  const [value, setValue] = React.useState<string[]>(['cheese', 'olives'])
  return (
    <div className="field">
      <div id="listbox-toppings-label" className="listbox-label">
        Toppings
      </div>
      <Listbox.Root
        className="listbox"
        multiple
        value={value}
        aria-labelledby="listbox-toppings-label"
        onValueChange={setValue}
      >
        {toppings.map((topping) => (
          <Listbox.Item
            key={topping.value}
            value={topping.value}
            className="listbox-item"
          >
            <Listbox.ItemIndicator className="listbox-item-indicator">
              ✓
            </Listbox.ItemIndicator>
            <Listbox.ItemText className="listbox-item-text">
              {topping.label}
            </Listbox.ItemText>
          </Listbox.Item>
        ))}
      </Listbox.Root>
      <p className="muted">Selected: {value.join(', ') || 'none'}</p>
    </div>
  )
}

Grouped

Group and GroupLabel add headings, and a Separator divides the groups.

ListboxGrouped.tsx
import * as React from 'react'
import { Listbox } from '@hamur/react/listbox'

const produce = [
  {
    value: 'Fruits',
    items: [
      { value: 'apple', label: 'Apple' },
      { value: 'banana', label: 'Banana' },
      { value: 'orange', label: 'Orange' },
    ],
  },
  {
    value: 'Vegetables',
    items: [
      { value: 'broccoli', label: 'Broccoli' },
      { value: 'carrot', label: 'Carrot' },
      { value: 'spinach', label: 'Spinach' },
    ],
  },
]

export default function ListboxGroupedExample() {
  return (
    <div className="field">
      <div id="listbox-produce-label" className="listbox-label">
        Produce
      </div>
      <Listbox.Root className="listbox" aria-labelledby="listbox-produce-label">
        {produce.map((group, index) => (
          <React.Fragment key={group.value}>
            <Listbox.Group className="listbox-group">
              <Listbox.GroupLabel className="listbox-group-label">
                {group.value}
              </Listbox.GroupLabel>
              {group.items.map((item) => (
                <Listbox.Item
                  key={item.value}
                  value={item.value}
                  className="listbox-item"
                >
                  <Listbox.ItemIndicator
                    className="listbox-item-indicator"
                  >
                    ✓
                  </Listbox.ItemIndicator>
                  <Listbox.ItemText className="listbox-item-text">
                    {item.label}
                  </Listbox.ItemText>
                </Listbox.Item>
              ))}
            </Listbox.Group>
            {index < produce.length - 1 ? (
              <Listbox.Separator className="listbox-separator" />
            ) : null}
          </React.Fragment>
        ))}
      </Listbox.Root>
    </div>
  )
}

Controlled

value and onValueChange keep the value in state; a button sets it from outside, and the readout shows the last details.reason (pointer or keyboard).

ListboxControlled.tsx
import * as React from 'react'
import { Listbox } from '@hamur/react/listbox'

const fruits = [
  { value: 'apple', label: 'Apple' },
  { value: 'banana', label: 'Banana' },
  { value: 'mango', label: 'Mango' },
]

export default function ListboxControlledExample() {
  const [value, setValue] = React.useState('banana')
  const [reason, setReason] = React.useState('none')
  return (
    <div className="field">
      <div id="listbox-controlled-label" className="listbox-label">
        Fruit
      </div>
      <Listbox.Root
        className="listbox"
        value={value}
        aria-labelledby="listbox-controlled-label"
        onValueChange={(next: string, details: { reason: string }) => { setValue(next) setReason(details.reason) }}
      >
        {fruits.map((fruit) => (
          <Listbox.Item
            key={fruit.value}
            value={fruit.value}
            className="listbox-item"
          >
            <Listbox.ItemIndicator className="listbox-item-indicator">
              ✓
            </Listbox.ItemIndicator>
            <Listbox.ItemText className="listbox-item-text">
              {fruit.label}
            </Listbox.ItemText>
          </Listbox.Item>
        ))}
      </Listbox.Root>
      <button className="button" type="button" onClick={() => setValue('mango')}>
        Pick Mango
      </button>
      <p className="muted">
        Value: {value}. Last reason: {reason}
      </p>
    </div>
  )
}

Horizontal

orientation="horizontal" makes a size picker that the left and right arrow keys move through; in a right-to-left layout they are mirrored.

ListboxHorizontal.tsx
import { Listbox } from '@hamur/react/listbox'

const sizes = ['XS', 'S', 'M', 'L', 'XL']

export default function ListboxHorizontalExample() {
  return (
    <div className="field">
      <div id="listbox-size-label" className="listbox-label">
        Size
      </div>
      <Listbox.Root
        className="listbox listbox-horizontal"
        orientation="horizontal"
        defaultValue="M"
        aria-labelledby="listbox-size-label"
      >
        {sizes.map((size) => (
          <Listbox.Item key={size} value={size} className="listbox-chip">
            <Listbox.ItemText>{size}</Listbox.ItemText>
          </Listbox.Item>
        ))}
      </Listbox.Root>
      <p className="muted">The left and right arrow keys move focus; right to left mirrors them.</p>
    </div>
  )
}

Disabled

disabled on Root takes the whole list out of the tab order; disabled on an Item keeps it out of focus, ranges and typeahead.

ListboxDisabled.tsx
import { Listbox } from '@hamur/react/listbox'

const shipping = [
  { value: 'standard', label: 'Standard' },
  { value: 'express', label: 'Express' },
  { value: 'overnight', label: 'Overnight' },
]
const days = [
  { value: 'monday', label: 'Monday' },
  { value: 'tuesday', label: 'Tuesday' },
  { value: 'saturday', label: 'Saturday', disabled: true },
  { value: 'sunday', label: 'Sunday', disabled: true },
]

export default function ListboxDisabledExample() {
  return (
    <div className="stack">
      <div className="field">
        <div id="listbox-shipping-label" className="listbox-label">
          Shipping (disabled)
        </div>
        <Listbox.Root
          className="listbox"
          disabled
          defaultValue="standard"
          aria-labelledby="listbox-shipping-label"
        >
          {shipping.map((item) => (
            <Listbox.Item
              key={item.value}
              value={item.value}
              className="listbox-item"
            >
              <Listbox.ItemIndicator className="listbox-item-indicator">
                ✓
              </Listbox.ItemIndicator>
              <Listbox.ItemText className="listbox-item-text">
                {item.label}
              </Listbox.ItemText>
            </Listbox.Item>
          ))}
        </Listbox.Root>
      </div>
      <div className="field">
        <div id="listbox-day-label" className="listbox-label">
          Delivery day
        </div>
        <Listbox.Root className="listbox" aria-labelledby="listbox-day-label">
          {days.map((item) => (
            <Listbox.Item
              key={item.value}
              value={item.value}
              disabled={item.disabled}
              className="listbox-item"
            >
              <Listbox.ItemIndicator className="listbox-item-indicator">
                ✓
              </Listbox.ItemIndicator>
              <Listbox.ItemText className="listbox-item-text">
                {item.label}
              </Listbox.ItemText>
            </Listbox.Item>
          ))}
        </Listbox.Root>
      </div>
    </div>
  )
}

Read Only

readOnly keeps the preselected value: focus and typeahead still move, presses do nothing.

ListboxReadOnly.tsx
import { Listbox } from '@hamur/react/listbox'

const plans = [
  { value: 'free', label: 'Free' },
  { value: 'pro', label: 'Pro' },
  { value: 'team', label: 'Team' },
]

export default function ListboxReadOnlyExample() {
  return (
    <div className="field">
      <div id="listbox-plan-label" className="listbox-label">
        Plan
      </div>
      <Listbox.Root
        className="listbox"
        readOnly
        defaultValue="pro"
        aria-labelledby="listbox-plan-label"
      >
        {plans.map((plan) => (
          <Listbox.Item
            key={plan.value}
            value={plan.value}
            className="listbox-item"
          >
            <Listbox.ItemIndicator className="listbox-item-indicator">
              ✓
            </Listbox.ItemIndicator>
            <Listbox.ItemText className="listbox-item-text">
              {plan.label}
            </Listbox.ItemText>
          </Listbox.Item>
        ))}
      </Listbox.Root>
      <p className="muted">Focus moves through the plans; the value stays Pro.</p>
    </div>
  )
}

Disallow Empty

disallowEmptySelection only has an effect with multiple: it keeps the last selected option from being released. A single listbox never deselects, like a native <select>.

ListboxDisallowEmpty.tsx
import * as React from 'react'
import { Listbox } from '@hamur/react/listbox'

const channels = [
  { value: 'email', label: 'Email' },
  { value: 'sms', label: 'SMS' },
  { value: 'push', label: 'Push' },
]

export default function ListboxDisallowEmptyExample() {
  const [value, setValue] = React.useState<string[]>(['email'])
  return (
    <div className="field">
      <div id="listbox-notify-label" className="listbox-label">
        Notify me by
      </div>
      <Listbox.Root
        className="listbox"
        multiple
        disallowEmptySelection
        value={value}
        aria-labelledby="listbox-notify-label"
        onValueChange={setValue}
      >
        {channels.map((channel) => (
          <Listbox.Item
            key={channel.value}
            value={channel.value}
            className="listbox-item"
          >
            <Listbox.ItemIndicator className="listbox-item-indicator">
              ✓
            </Listbox.ItemIndicator>
            <Listbox.ItemText className="listbox-item-text">
              {channel.label}
            </Listbox.ItemText>
          </Listbox.Item>
        ))}
      </Listbox.Root>
      <p className="muted">Selected: {value.join(', ')}</p>
    </div>
  )
}

Empty

Empty shows "No results" while there are no items, and the root gets data-empty; a button adds and removes the items.

ListboxEmpty.tsx
import * as React from 'react'
import { Listbox } from '@hamur/react/listbox'

const results = [
  { value: 'report', label: 'Report.pdf' },
  { value: 'budget', label: 'Budget.xlsx' },
  { value: 'notes', label: 'Notes.txt' },
]

export default function ListboxEmptyExample() {
  const [shown, setShown] = React.useState(false)
  return (
    <div className="field">
      <div id="listbox-results-label" className="listbox-label">
        Results
      </div>
      <Listbox.Root className="listbox" aria-labelledby="listbox-results-label">
        <Listbox.Empty className="listbox-empty">
          No results
        </Listbox.Empty>
        {shown
          ? results.map((result) => (
              <Listbox.Item
                key={result.value}
                value={result.value}
                className="listbox-item"
              >
                <Listbox.ItemIndicator
                  className="listbox-item-indicator"
                >
                  ✓
                </Listbox.ItemIndicator>
                <Listbox.ItemText className="listbox-item-text">
                  {result.label}
                </Listbox.ItemText>
              </Listbox.Item>
            ))
          : null}
      </Listbox.Root>
      <button className="button" type="button" onClick={() => setShown(!shown)}>
        {shown ? 'Remove items' : 'Add items'}
      </button>
    </div>
  )
}

Form

Inside Field and Form: name comes from the Field, required blocks the submit until a size is chosen, and the multiple listbox submits toppings=cheese&toppings=olives.

ListboxForm.tsx
import * as React from 'react'
import { Field } from '@hamur/react/field'
import { Form } from '@hamur/react/form'
import { Listbox } from '@hamur/react/listbox'

const sizes = [
  { value: 'small', label: 'Small' },
  { value: 'medium', label: 'Medium' },
  { value: 'large', label: 'Large' },
]
const toppings = [
  { value: 'cheese', label: 'Cheese' },
  { value: 'olives', label: 'Olives' },
  { value: 'peppers', label: 'Peppers' },
]

export default function ListboxFormExample() {
  const [submitted, setSubmitted] = React.useState('')
  return (
    <Form
      className="stack"
      onFormSubmit={(_values, details) => {
        const form = details.event.target as HTMLFormElement
        // A multiple listbox submits one entry per value: toppings=cheese&toppings=olives.
        setSubmitted([...new FormData(form)].map(([key, value]) => `${key}=${value}`).join('&'))
      }}
    >
      <Field.Root name="size" className="field">
        <Field.Label className="listbox-label">Size</Field.Label>
        <Listbox.Root className="listbox" required>
          {sizes.map((size) => (
            <Listbox.Item
              key={size.value}
              value={size.value}
              className="listbox-item"
            >
              <Listbox.ItemIndicator className="listbox-item-indicator">
                ✓
              </Listbox.ItemIndicator>
              <Listbox.ItemText className="listbox-item-text">
                {size.label}
              </Listbox.ItemText>
            </Listbox.Item>
          ))}
        </Listbox.Root>
        <Field.Error className="error" />
      </Field.Root>
      <Field.Root name="toppings" className="field">
        <Field.Label className="listbox-label">Toppings</Field.Label>
        <Listbox.Root
          className="listbox"
          multiple
          defaultValue={['cheese', 'olives']}
        >
          {toppings.map((topping) => (
            <Listbox.Item
              key={topping.value}
              value={topping.value}
              className="listbox-item"
            >
              <Listbox.ItemIndicator className="listbox-item-indicator">
                ✓
              </Listbox.ItemIndicator>
              <Listbox.ItemText className="listbox-item-text">
                {topping.label}
              </Listbox.ItemText>
            </Listbox.Item>
          ))}
        </Listbox.Root>
      </Field.Root>
      <button className="button" type="submit">
        Order
      </button>
      {submitted ? <pre className="output">{submitted}</pre> : null}
    </Form>
  )
}

Object Values

Items hold objects: isItemEqualToValue matches the value by id, and itemToStringValue gives the id the form submits.

ListboxObjectValues.tsx
import * as React from 'react'
import { Listbox } from '@hamur/react/listbox'

interface Method {
  id: string
  name: string
  days: string
}

const methods: Method[] = [
  { id: 'standard', name: 'Standard', days: '5 to 7 days' },
  { id: 'express', name: 'Express', days: '2 days' },
  { id: 'overnight', name: 'Overnight', days: 'Next day' },
]

export default function ListboxObjectValuesExample() {
  // A copy, not the item itself: isItemEqualToValue matches it by id.
  const [value, setValue] = React.useState<Method>({ ...methods[1] })
  return (
    <div className="field">
      <div id="listbox-method-label" className="listbox-label">
        Shipping method
      </div>
      <Listbox.Root
        className="listbox"
        name="shipping"
        value={value}
        isItemEqualToValue={(item: Method, current: Method) => item.id === current.id}
        itemToStringValue={(item: Method) => item.id}
        aria-labelledby="listbox-method-label"
        onValueChange={setValue}
      >
        {methods.map((method) => (
          <Listbox.Item
            key={method.id}
            value={method}
            label={method.name}
            className="listbox-item"
          >
            <Listbox.ItemIndicator className="listbox-item-indicator">
              ✓
            </Listbox.ItemIndicator>
            <Listbox.ItemText className="listbox-item-text">
              {method.name} <span className="muted">{method.days}</span>
            </Listbox.ItemText>
          </Listbox.Item>
        ))}
      </Listbox.Root>
      <p className="muted">
        Value: {value.name}. Submits shipping={value.id}
      </p>
    </div>
  )
}

Highlight

highlightedValue and onHighlightChange report the option that holds focus, and null once focus leaves.

ListboxHighlight.tsx
import * as React from 'react'
import { Listbox } from '@hamur/react/listbox'

const colors = [
  { value: 'red', label: 'Red' },
  { value: 'green', label: 'Green' },
  { value: 'blue', label: 'Blue' },
  { value: 'yellow', label: 'Yellow' },
]

export default function ListboxHighlightExample() {
  const [highlighted, setHighlighted] = React.useState<string | null>(null)
  return (
    <div className="field">
      <div id="listbox-color-label" className="listbox-label">
        Color
      </div>
      <Listbox.Root
        className="listbox"
        highlightedValue={highlighted}
        aria-labelledby="listbox-color-label"
        onHighlightChange={setHighlighted}
      >
        {colors.map((color) => (
          <Listbox.Item
            key={color.value}
            value={color.value}
            className="listbox-item"
          >
            <Listbox.ItemIndicator className="listbox-item-indicator">
              ✓
            </Listbox.ItemIndicator>
            <Listbox.ItemText className="listbox-item-text">
              {color.label}
            </Listbox.ItemText>
          </Listbox.Item>
        ))}
      </Listbox.Root>
      <p className="muted">Highlighted: {highlighted ?? 'none'}</p>
    </div>
  )
}

Virtualized

1,000 options, of which only the rows near the scroll position render; items and virtualized make Home, End, PageUp, PageDown, looping, typeahead and ranges work on all of them, and onItemHighlighted scrolls the target into view.

ListboxVirtualized.tsx
import * as React from 'react'
import { Listbox } from '@hamur/react/listbox'

// Rows of a fixed height, windowed by hand: only the rows near the scroll position render.
const ROW = 32
const VIEWPORT = 260
const OVERSCAN = 10
const COUNT = 1000

// Every item, mounted or not: Home, End, Page Up, Page Down, looping, typeahead and ranges work on it.
const items = Array.from({ length: COUNT }, (_, index) => index + 1)
const getLabel = (item: number) => `Item ${String(item).padStart(4, '0')}`

export default function ListboxVirtualizedExample() {
  const scrollerRef = React.useRef<HTMLDivElement | null>(null)
  const [scrollTop, setScrollTop] = React.useState(0)
  const first = Math.max(0, Math.floor(scrollTop / ROW) - OVERSCAN)
  const last = Math.min(COUNT, Math.ceil((scrollTop + VIEWPORT) / ROW) + OVERSCAN)
  // Like Combobox: the listbox asks for the item at `index` to be scrolled into view, then focuses
  // it once its row has mounted.
  function scrollToIndex(index: number) {
    const scroller = scrollerRef.current
    if (!scroller) {
      return
    }
    const top = index * ROW
    if (top < scroller.scrollTop) {
      scroller.scrollTop = top
    } else if (top + ROW > scroller.scrollTop + scroller.clientHeight) {
      scroller.scrollTop = top + ROW - scroller.clientHeight
    }
    setScrollTop(scroller.scrollTop)
  }

  return (
    <div className="field">
      <div id="listbox-virtual-label" className="listbox-label">
        1,000 items
      </div>
      <div
        ref={scrollerRef}
        className="listbox-scroller"
        onScroll={(event) => setScrollTop(event.currentTarget.scrollTop)}
      >
        <Listbox.Root
          className="listbox listbox-virtual"
          items={items}
          virtualized
          itemToStringLabel={getLabel}
          onItemHighlighted={(item, { index }) => scrollToIndex(index)}
          style={{ height: COUNT * ROW }}
          aria-labelledby="listbox-virtual-label"
        >
          {items.slice(first, last).map((item, offset) => (
            <Listbox.Item
              key={item}
              value={item}
              index={first + offset}
              className="listbox-item"
              style={{ transform: `translateY(${(first + offset) * ROW}px)` }}
            >
              <Listbox.ItemIndicator className="listbox-item-indicator">
                ✓
              </Listbox.ItemIndicator>
              <Listbox.ItemText className="listbox-item-text">
                {getLabel(item)}
              </Listbox.ItemText>
            </Listbox.Item>
          ))}
        </Listbox.Root>
      </div>
    </div>
  )
}

API reference

31 props across 8 parts, in React syntax. className, style, render are listed once, under Shared props.

EmptyListbox.Empty

Content shown while the listbox has no items. Renders nothing otherwise. It has no ARIA role, since only options and groups may sit inside a listbox. Renders a <div>.

GroupListbox.Group

Groups related listbox items with the corresponding label. Renders a <div>.

GroupLabelListbox.GroupLabel

An accessible label that is automatically associated with its parent group. Renders a <div>.

ItemListbox.Item

An individual option in the listbox. Renders a <div>.

Item props
PropTypeDefault
childrenReact.ReactNode—
valueA unique value that identifies this item.anynull
labelOverrides the text that typeahead matches.string—
indexThe zero-based position of the item in the full list.number—
countThe size of the full list, for aria-setsize.number—
disabledWhether the component should ignore user interaction.booleanfalse
Item data attributes
Data attributeDescription
data-selectedPresent when the listbox item is selected.
data-highlightedPresent when the listbox item is highlighted (holds focus).
data-disabledPresent when the listbox item is disabled.

ItemIndicatorListbox.ItemIndicator

Indicates whether the listbox item is selected. Renders a <span>.

ItemIndicator props
PropTypeDefault
childrenReact.ReactNode—
keepMountedWhether to keep the HTML element in the DOM when the item is not selected.boolean—
ItemIndicator data attributes
Data attributeDescription
data-starting-stylePresent when the indicator begins animating in.
data-ending-stylePresent when the indicator is animating out.

ItemTextListbox.ItemText

A text label of the listbox item. Renders a <div>.

RootListbox.Root

Groups all parts of an always visible list of options. Renders a <div>.

Root props
PropTypeDefault
valueThe value of the listbox.ListboxInputValue<Value, Multiple> | null—
defaultValueThe uncontrolled value of the listbox when it's initially rendered.ListboxInputValue<Value, Multiple> | null—
onValueChangeEvent handler called when the value changes.((value: ListboxOutputValue<Value, Multiple>, eventDetails: ListboxRootChangeEventDetails) => void)—
multipleWhether multiple items can be selected.Multiplefalse
disabledWhether the component should ignore user interaction.booleanfalse
readOnlyWhether the user should be unable to change the selection.booleanfalse
requiredWhether the user must choose a value before submitting a form.booleanfalse
disallowEmptySelectionWhether the last selected item of a multiple listbox cannot be deselected, so the listboxbooleanfalse
highlightedValueThe value of the highlighted item (the one that holds focus).Value | null—
onHighlightChangeEvent handler called when the highlighted item changes.((value: Value | null) => void)—
nameIdentifies the field when a form is submitted.string—
formIdentifies the form that owns the hidden input.string—
orientationThe orientation of the listbox.Orientation'vertical'
loopFocusWhether to loop keyboard focus back to the first item when the end of the list is reachedbooleantrue
isItemEqualToValueCustom comparison logic used to determine if an item value matches the current value.((itemValue: Value, value: Value) => boolean)—
itemToStringValueWhen the item values are objects, this function converts the object value to a string((itemValue: Value) => string)—
itemsEvery item of the list, mounted or not.readonly Value[]—
virtualizedWhether the items are rendered by an external virtualizer, so only some of items arebooleanfalse
itemToStringLabelWhen the items are objects, converts an item to the text typeahead matches.((itemValue: Value) => string)—
isItemDisabledWhether an item of items is disabled, for items that are not mounted.((itemValue: Value) => boolean)—
onItemHighlightedVirtualized only: called when keyboard navigation (or highlightedValue) is about to move((itemValue: Value, eventDetails: ListboxRootHighlightEventDetails) => void)—
inputRefA ref to access the hidden input element.React.Ref<HTMLInputElement>—
Root data attributes
Data attributeDescription
data-disabledPresent when the listbox is disabled.
data-readonlyPresent when the listbox does not allow changing the selection.
data-requiredPresent when the listbox is required.
data-multiplePresent when the listbox allows multiple selected items.
data-orientationIndicates the orientation of the listbox.
data-emptyPresent when the listbox has no items.

SeparatorListbox.Separator

A visual separator between items or groups. Renders a <div>.

Separator props
PropTypeDefault
orientationThe orientation of the separator.Orientation'horizontal'
Separator data attributes
Data attributeDescription
data-orientationIndicates the orientation of the separator.

Shared props

Every part that renders an element takes these.

Shared props
PropType
classNameCSS class applied to the element, or a function that returns a class based on the component's state.string | ((state: State) => string | undefined)
styleStyle applied to the element, or a function that returns a style object based on the component's state.React.CSSProperties | ((state: State) => React.CSSProperties | undefined)
renderAllows you to replace the component's HTML element with a different tag, or compose it with another component.React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, State>

Keyboard and accessibility

Keyboard

Keyboard
KeysDoesPart
ArrowDownArrowUpHomeEndMoves focus between the options without selecting (ArrowRight and ArrowLeft when horizontal)Root
PageDownPageUpMoves focus ten options and stops at the endsRoot
Moves focus to the next option whose text starts with what was typed, without selectingRoot
EnterSpaceSelects the focused option; with multiple, toggles itItem
ShiftArrowDownShiftArrowUpWith multiple, extends the selection from the last pressed optionRoot
ShiftHomeShiftEndWith multiple, extends the selection to the first or last optionRoot
ShiftSpaceWith multiple, selects the range from the last pressed option to the focused oneRoot
ControlAMetaAWith multiple, selects every enabled option, or clears them when all are selectedRoot

Accessibility

What the parts set
PartSetsSo that
Rootrole="listbox", aria-multiselectable with multiple, aria-orientationthe options are read as one list, and whether several can be chosen
Rootaria-disabled, aria-readonly, aria-requiredthe list is read with its constraints; name it with aria-labelledby
Itemrole="option", aria-selected, aria-disabledeach option is read with its selected and disabled state
Grouprole="group", aria-labelledby to its GroupLabelgrouped options are read under their heading
GroupLabelaria-hidden="true"the heading is read once, as the group's name
Separatorrole="presentation"the divider is not read; only options and groups may sit in a listbox

Focus order

  1. The listbox is one tab stop: the first selected option, else the first enabled one.
  2. Arrow keys, Home, End, PageUp and PageDown move focus between options and skip disabled ones; with loopFocus the arrows wrap at the ends. With virtualized, these keys work on items, not on the rows that are mounted.
  3. Tab leaves the listbox.