Select
Single-value select with a combobox trigger, popup listbox, option collection, scroll controls, portal, and a visually hidden native form control.
Select in motion
When to Use
Use Select when the user chooses one value from a list and the choices should
stay hidden until needed. Use RadioGroup when a short list should remain
visible for easy comparison, and use Combobox when users need to type to
filter or enter a value.
Features
The behavior Atom owns before your product adds appearance.
- Controlled and uncontrolled selected value.
- Controlled and uncontrolled open state.
- Keyboard navigation, typeahead search, highlighting, and selection.
- Group, label, separator, viewport, scroll buttons, item text, and item indicator parts.
- Native select for submission, required validity, external form association, and reset behavior.
- Stack-aware Escape dismissal when nested inside parent overlays.
- Integrates with
Field.Rootfor trigger labels, descriptions, disabled state, and required state. - Optional portal and popup arrow.
Import
Anatomy
API Reference
Root
Owns value, open state, item registration, form submission, and Field
integration. Root renders no DOM wrapper except its hidden native select when
name is provided.
| Prop | Type | Default |
|---|---|---|
value | string | - |
defaultValue | string | - |
onValueChange | (value: string) => void | - |
open | boolean | - |
defaultOpen | boolean | false |
onOpenChange | (open: boolean) => void | - |
disabled | boolean | false |
readOnly | boolean | Field state or false |
invalid | boolean | Field state or false |
required | boolean | false |
validationBehavior | "inline" | "native" | Field/Form value or "native" |
name | string | - |
form | string | - |
ARIA: Root renders no semantic element. Trigger and Content own the ARIA contract.
Data attributes: Root renders no wrapper and exposes none.
When used inside Field.Root, disabled, read-only, invalid, and required state
default to Field unless explicitly provided on Select.Root.
Trigger
Combobox button that opens the listbox and owns keyboard interaction.
| Prop | Type | Default |
|---|---|---|
asChild | boolean | false |
render | RenderProp | - |
| ARIA attribute | Values |
|---|---|
role | "combobox" |
aria-haspopup | "listbox" |
aria-expanded | Current open state |
aria-controls | Generated Content/Listbox ID |
aria-activedescendant | Highlighted Item ID while open |
aria-label | Explicit native value |
aria-labelledby | Explicit IDs or inherited Field label ID |
aria-describedby | Explicit IDs or inherited Field description/error IDs |
aria-disabled | true when disabled |
aria-required | true when required |
aria-readonly | true when read only |
aria-invalid | true when invalid |
| Data attribute | Values |
|---|---|
[data-slot] | "select-trigger" |
[data-state] | "open" | "closed" |
[data-disabled] | Present when disabled |
When used inside Field.Root, Trigger uses the Field control ID and inherits
aria-labelledby / aria-describedby from the Field label and visible
description or error content. Explicit id, aria-label, aria-labelledby,
and aria-describedby props override this wiring.
Typing a printable character while the trigger is focused opens the listbox and highlights the first enabled item whose label starts with the typed text.
Value
Displays the registered text for the selected Item, or the placeholder when no
value is selected. It renders a span.
| Prop | Type | Default |
|---|---|---|
placeholder | ReactNode | - |
ARIA: Value adds no ARIA attributes; Trigger owns the control's name and value relationship.
| Data attribute | Values |
|---|---|
[data-slot] | "select-value" |
[data-placeholder] | Present when no selected label is available |
Icon
Provides a decorative span for a consumer-supplied trigger icon.
| Prop | Type | Default |
|---|---|---|
Props: Icon has no Atom-owned behavior props and accepts native span | ||
| props. |
| ARIA attribute | Values |
|---|---|
aria-hidden | Always true |
| Data attribute | Values |
|---|---|
[data-slot] | "select-icon" |
Portal
Moves Content to another container and tells Select that Content is already in a portal, preventing a second portal wrapper.
| Prop | Type | Default |
|---|---|---|
container | HTMLElement | null | document.body after mount |
disabled | boolean | false |
ARIA: Portal renders no wrapper and adds no ARIA attributes.
Data attributes: Portal renders no wrapper and exposes none.
Content
Public alias of Listbox that renders and positions the popup option container. It owns dismissal, initial highlighting, and focus-scope registration.
| Prop | Type | Default |
|---|---|---|
disablePortal | boolean | false |
container | HTMLElement | null | document.body after mount |
dir | \"ltr\" | \"rtl\" | Computed Trigger direction, then Direction provider |
onInteractOutside | (event: OutsideInteractionEvent) => void | - |
Outside dismissal commits on completed click/activation semantics. Calling
event.preventDefault() keeps Content open without cancelling the original
destination click; dragged, cancelled, secondary-button, and multi-pointer
sessions do not dismiss.
| ARIA attribute | Values |
|---|---|
role | "listbox" |
aria-label | Native value when provided |
| Data attribute | Values |
|---|---|
[data-slot] | "select-listbox" |
[data-state] | "open" while rendered |
[data-positioned] | Present after the first positioning frame |
[data-side] | "top" | "right" | "bottom" | "left" after collision handling |
[data-align] | "start" | "center" | "end" after collision handling |
ScrollUpButton
Renders only while the Viewport can scroll upward. It scrolls the option area without entering the keyboard focus order.
Props: Accepts native button props and children; it has no additional Atom-owned behavior props.
| ARIA attribute | Values |
|---|---|
aria-hidden | Always true |
| Data attribute | Values |
|---|---|
[data-slot] | "select-scroll-up-button" |
Viewport
Provides the scroll container registered with both scroll buttons. It renders a
div and accepts native div props.
ARIA: Viewport adds no roles or ARIA attributes.
| Data attribute | Values |
|---|---|
[data-slot] | "select-viewport" |
Group
Wraps related Items with role="group" and connects them to its Label.
Props: Group has no Atom-owned behavior props and accepts native div
props.
| ARIA attribute | Values |
|---|---|
role | "group" |
aria-labelledby | Generated Label ID |
| Data attribute | Values |
|---|---|
[data-slot] | "select-group" |
Label
Names its nearest Group through a generated or consumer-provided ID.
Props: Label has no Atom-owned behavior props and accepts native div
props, including id.
ARIA: Label adds no ARIA attributes; Group references its ID.
| Data attribute | Values |
|---|---|
[data-slot] | "select-label" |
Item
Registers one value and text label, owns pointer highlighting and selection, and returns focus to Trigger after selection.
| Prop | Type | Default |
|---|---|---|
value | string | Required |
disabled | boolean | false |
label | string | - |
| ARIA attribute | Values |
|---|---|
role | "option" |
aria-selected | true when selected |
aria-disabled | true when disabled |
aria-labelledby | ItemText ID when ItemText is mounted |
| Data attribute | Values |
|---|---|
[data-slot] | "select-item" |
[data-state] | "checked" | "unchecked" |
[data-highlighted] | Present when highlighted |
[data-disabled] | Present when disabled |
[data-value] | Option value |
ItemText
Registers the visible option text for Trigger display, typeahead, and Item's
accessible name. It renders a span.
Props: ItemText has no Atom-owned behavior props and accepts native span
props.
ARIA: ItemText adds no ARIA attributes; Item references its generated ID.
| Data attribute | Values |
|---|---|
[data-slot] | "select-item-text" |
ItemIndicator
Renders a decorative selected marker only for the current Item unless
forceMount keeps it mounted for stable composition.
| Prop | Type | Default |
|---|---|---|
forceMount | boolean | false |
| ARIA attribute | Values |
|---|---|
aria-hidden | Always true |
| Data attribute | Values |
|---|---|
[data-slot] | "select-item-indicator" |
[data-state] | "checked" | "unchecked" |
Separator
Renders a horizontal separator between option sections.
Props: Separator has no Atom-owned behavior props and accepts native div
props.
| ARIA attribute | Values |
|---|---|
role | "separator" |
aria-orientation | "horizontal" |
| Data attribute | Values |
|---|---|
[data-slot] | "select-separator" |
ScrollDownButton
Renders only while the Viewport can scroll downward. It scrolls the option area without entering the keyboard focus order.
Props: Accepts native button props and children; it has no additional Atom-owned behavior props.
| ARIA attribute | Values |
|---|---|
aria-hidden | Always true |
| Data attribute | Values |
|---|---|
[data-slot] | "select-scroll-down-button" |
Arrow
Provides a decorative span hook positioned against the trigger by the same
Floating UI calculation as Content. Consumers draw the arrow and may size it
with CSS; Atom owns its collision-aware physical edge and coordinates.
Props: Arrow has no Atom-owned behavior props, accepts optional decorative
children, and forwards native span props.
| ARIA attribute | Values |
|---|---|
aria-hidden | Always true |
| Data attribute | Values |
|---|---|
[data-slot] | "select-arrow" |
[data-side] | Actual Content side after collision handling |
[data-align] | Actual Content alignment after collision handling |
Arrow must be inside Content/Listbox. Atom composes its ref with the positioning
middleware and applies absolute positioning through inline top/left plus
the appropriate static edge. Consumer children, className, native span
props, and non-positioning style values remain supported.
Listbox
Alternative public name for Content with the same positioned listbox behavior.
Use either Content or Listbox, not both for the same popup.
| Prop | Type | Default |
|---|---|---|
disablePortal | boolean | false |
container | HTMLElement | null | document.body after mount |
| ARIA attribute | Values |
|---|---|
role | "listbox" |
aria-label | Native value when provided |
| Data attribute | Values |
|---|---|
[data-slot] | "select-listbox" |
[data-state] | "open" while rendered |
[data-positioned] | Present after the first positioning frame |
[data-side] | Actual physical side after collision handling |
[data-align] | Actual alignment after collision handling |
Advanced compound parts can read useSelectContext, useSelectItemContext,
or useSelectGroupContext. Their matching public providers expose the same
contracts for low-level composition.
Examples
Form Select
Grouped Options
Accessibility
The aligned native select owns required validity. A validation attempt mirrors its state to Trigger and Field. Inline behavior suppresses the browser bubble; native behavior keeps it and redirects focus to Trigger.
Select follows the
WAI-ARIA select-only combobox pattern:
a button-based
combobox controls a listbox, and Trigger references the highlighted option with
aria-activedescendant. Provide a visible Field label or native
aria-label/aria-labelledby. Uncontrolled value returns to defaultValue
on native form reset. The transparent native select owns submission and
required constraint validation, is aligned with Trigger, works when required
without a submission name, and redirects browser validation focus to Trigger.
aria-labelledby.
Portalled Select content registers with a parent modal focus scope when opened
inside Dialog, Drawer, or another modal primitive.
Content preserves an explicit dir or resolves the Trigger/Direction-provider
direction across its portal boundary, so logical alignment and option content
remain correct in RTL.
Printable-character typeahead matches enabled option text; a single-character
search cycles forward from the current matching option, while multi-character
buffers match exact prefixes.
| Key | Description |
|---|---|
ArrowDown | Opens the listbox or moves to the next enabled item. |
ArrowUp | Opens the listbox from the last item or moves to the previous enabled item. |
Enter | Opens the listbox or selects the highlighted item. |
Space | Opens the listbox or selects the highlighted item. |
Home | Highlights the first enabled item. |
End | Highlights the last enabled item. |
| Printable character | Typeahead search. |
Escape | Closes the listbox. |
Tab | Closes the listbox and moves focus normally. |
Changelog
Unreleased
0.24.0
- Added public Agent Knowledge for component selection, required composition, recurring mistakes, and validation.
0.20.4
- Content/Listbox now resolves direction from its explicit
dir, Trigger, or Direction provider and applies it across the portal boundary, preserving logical placement and option layout in RTL.
0.20.0
- Added preventable
Content/Listbox.onInteractOutsideand moved outside dismissal to the shared layer-aware completed-activation contract.
0.9.3
- Fixed Arrow's public TypeScript props to accept replaceable decorative children.
0.9.2
- Deferred touch and pen outside dismissal until the gesture resolves as a tap, preserving the open Select during movement, cancellation, or scrolling.
- Positioned Arrow through the Select Floating UI calculation and exposed
collision-resolved
data-sideanddata-alignon Content and Arrow.
0.6.16
- Explicitly scrolled inline validation-directed focus into view.
0.6.15
- Exposed inline validation-directed focus through
[data-focus-visible]until blur.
0.6.13
- Mirrored aligned native-select validity to Trigger, Field, and Form under the shared inline/native validation contract.
0.6.12
- Aligned the native select with Trigger and redirected browser validation focus, including required Selects without a submission name.
0.5.0
- Added complete Field invalid/read-only integration, native-only Trigger and Listbox naming, uncontrolled reset, and a visually hidden native select for submission and required constraint validation.
0.2.0
- Fixed Select part
data-slotpass-through so Value, Icon, Content/Listbox, Viewport, Group, Label, Item, ItemText, ItemIndicator, Separator, Arrow, and scroll buttons can be overridden consistently. - Standardized Select typeahead so a single-character search cycles from the current matching option while multi-character buffers still match exact prefixes.
- Added shared dismissable layer Escape handling so Select closes before parent overlays when nested inside Dialog, Drawer, Modal, or Popover.
- Fixed outside pointer dismissal so Select closes reliably when clicking outside the trigger or listbox during inspection-heavy renders.
- Fixed value display so selected option labels resolve on initial closed render and remain stable after the listbox unmounts.
- Fixed trigger
asChildcomposition so the trigger does not render a nested copy of its child. - Fixed keyboard opening so
ArrowDown,ArrowUp,Home,End,Enter, andSpaceapply the intended initial highlight after portalled listbox items mount, keepingaria-activedescendantin sync. - Fixed closed-state typeahead so typing a matching character opens the listbox with the matching enabled item highlighted before items mount.
- Registered portalled Select content with parent modal focus scopes so Select can remain a valid focus target inside Dialog, Drawer, and other modal primitives.
- Added Field integration so
Select.Triggerinherits Field labels and descriptions whileSelect.Rootinherits Field disabled and required state. - Refined
Select.Triggerkeyboard handler dependencies to avoid recreating callbacks from the full context object.
0.1.0
- Initial Atom release.