Select

A headless, accessible select built on the WAI-ARIA Listbox pattern with single/multiple selection and nested submenus.

Features

  • Single or multiple selection
  • Controlled or uncontrolled open state
  • Grouped options
  • Nested submenus
  • Typeahead navigation
  • Dismissible popover

Installation

npm install @ariaui/select

Examples

Three playgrounds ported from the legacy doc: uncontrolled single-select (with and without a default), controlled single- and multi-select with groups plus a nested submenu, and uncontrolled multi-select with a default array.

Styling uses shared class strings from selectExampleTokens.ts and @heroicons/react/24/solid icons.

Uncontrolled

An uncontrolled select with an initial value.

Preview

No preview output yet.

live.tsxReady

Disabled

Disabled select: disabled on the root locks the trigger (Pro Blocks-style “Disabled” placeholder label).

Preview

No preview output yet.

live.tsxReady

With icon

Leading icon + chevron trigger and icon + label rows in the panel (chart-style options; Pro Blocks “With Icon”).

Preview

No preview output yet.

live.tsxReady

Large list + scroll area

A long flat option list capped with ScrollArea viewport height (~seven visible rows) for the Pro Blocks “large list” pattern.

Preview

No preview output yet.

live.tsxReady

Grouped With Submenu

A controlled single-select with grouped options and a nested submenu.

Preview

No preview output yet.

live.tsxReady

Grouped Multiple With Submenu

A controlled multi-select with grouped options and a nested submenu.

Preview

No preview output yet.

live.tsxReady

Multiple Selection (Uncontrolled)

An uncontrolled multi-select with default values and a trigger summary of selected labels.

Preview

No preview output yet.

live.tsxReady

Framer Motion

Use Content asChild and SubContent asChild to slot select listbox props onto Framer Motion surfaces.

Preview

No preview output yet.

live.tsxReady

Framer Motion + Scroll Area

A Select dropdown using the large-list ScrollArea pattern as a standalone example.

Preview

No preview output yet.

live.tsxReady

Anatomy

tsx
import * as Select from "@ariaui/select";

export default function Example() {
  return (
    <Select.Root>
      <Select.Trigger />
      <Select.Content>
        <Select.Label />
        <Select.Group>
          <Select.GroupLabel />
          <Select.Option />
        </Select.Group>
        <Select.Sub>
          <Select.SubTrigger />
          <Select.SubContent>
            <Select.Option />
          </Select.SubContent>
        </Select.Sub>
      </Select.Content>
    </Select.Root>
  );
}

API Reference

Root

Context provider and state container for the select. Coordinates open state, selection, and the floating content position.

PropTypeDefault
selectionMode
'single' | 'multiple''single'
value
string | string[]
defaultValue
string | string[]
onValueChange
(value: string | string[]) => void
open
boolean
defaultOpen
booleanfalse
onOpenChange
(open: boolean) => void
offset
{ x: number; y: number }{ x: 0, y: 0 }
disabled
booleanfalse

Trigger

Button that opens the select. Exposes the current state for styling and aria.

AttributeValues
aria-haspopup'listbox'
aria-expandedtrue | false
aria-controlsid of the Content element
data-state'open' | 'closed'

Content

Floating surface containing Options, Groups, and nested submenus. Portaled and positioned relative to Trigger.

PropTypeDefault
asChild
booleanfalse
AttributeValues
role'listbox'
aria-multiselectabletrue | false
aria-activedescendantid of the active Option
data-state'open' | 'closed'

Label

Accessible label for the select. Referenced by Content via aria-labelledby.

Option

A selectable item inside Content.

PropTypeDefault
value*
string
disabled
booleanfalse
asChild
booleanfalse
AttributeValues
role'option'
aria-selectedtrue | false
aria-disabledtrue when disabled
data-activetrue when focused / highlighted

Group

Groups related Options. Associates with GroupLabel via aria-labelledby.

AttributeValues
role'group'
aria-labelledbyid of the GroupLabel

GroupLabel

Label for a Group of Options.

Sub

Container for a submenu composed of SubTrigger and SubContent.

PropTypeDefault
offset
{ x: number; y: number }{ x: 0, y: 0 }
open
boolean
defaultOpen
booleanfalse
onOpenChange
(open: boolean) => void

SubTrigger

Trigger that opens a nested submenu on hover or ArrowRight.

AttributeValues
data-state'open' | 'closed'
data-activetrue when focused / highlighted

SubContent

Submenu surface containing nested Options.

PropTypeDefault
asChild
booleanfalse
AttributeValues
data-state'open'

Keyboard

ShortcutAction
Enter/Space//Open Content with focus on the selected (or first) Option when the Trigger is focused.
Move the active Option to the next enabled Option.
Move the active Option to the previous enabled Option.
HomeMove the active Option to the first enabled Option.
EndMove the active Option to the last enabled Option.
Open the focused submenu via SubTrigger and move focus into it.
Close the current submenu and return focus to its SubTrigger.
Enter/SpaceSelect the active Option (toggles in multiple mode) and close single-selection Content.
EscClose Content and return focus to Trigger.
TabClose Content and move focus to the next focusable element.

Accessibility

Select follows the WAI-ARIA Listbox (collapsible) pattern:

  • Trigger exposes aria-haspopup="listbox", aria-expanded, and aria-controls pointing at Content.
  • Content renders with role="listbox" and manages active descendants via aria-activedescendant — no per-option focus shuffling.
  • aria-multiselectable is set automatically when selectionMode="multiple", and each Option exposes aria-selected.
  • Options with disabled carry aria-disabled="true" and are skipped by keyboard navigation and selection.
  • Provide a label via Label or an aria-label / aria-labelledby on Root so assistive tech can announce the field.
  • Nested Sub menus open with hover or ArrowRight, close with ArrowLeft / Escape, and return focus to the SubTrigger.
  • Content is portaled so it escapes ancestor clipping and stacking contexts; click-outside dismissal returns focus to Trigger when appropriate.
Previous
Scroll Area