Select
Primitive

Select

Single-value select with a combobox trigger, popup listbox, option collection, scroll controls, portal, and a visually hidden native form control.

Live behavior

Select in motion

Interactive
Atom behavior · App-owned appearance
Preparing behavior…

Open the listbox and choose an option with the keyboard.

waiting for input

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.Root for trigger labels, descriptions, disabled state, and required state.
  • Optional portal and popup arrow.

Import

tsx
import { Select } from "@flowstack-ui/atom";

Anatomy

tsx
<Select.Root>
  <Select.Trigger>
    <Select.Value />
    <Select.Icon />
  </Select.Trigger>
  <Select.Portal>
    <Select.Content>
      <Select.ScrollUpButton />
      <Select.Viewport>
        <Select.Group>
          <Select.Label />
          <Select.Item>
            <Select.ItemText />
            <Select.ItemIndicator />
          </Select.Item>
        </Select.Group>
        <Select.Separator />
      </Select.Viewport>
      <Select.ScrollDownButton />
      <Select.Arrow />
    </Select.Content>
  </Select.Portal>
</Select.Root>

<Select.Listbox />

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.

PropTypeDefault
valuestring-
defaultValuestring-
onValueChange(value: string) => void-
openboolean-
defaultOpenbooleanfalse
onOpenChange(open: boolean) => void-
disabledbooleanfalse
readOnlybooleanField state or false
invalidbooleanField state or false
requiredbooleanfalse
validationBehavior"inline" | "native"Field/Form value or "native"
namestring-
formstring-

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.

PropTypeDefault
asChildbooleanfalse
renderRenderProp-
ARIA attributeValues
role"combobox"
aria-haspopup"listbox"
aria-expandedCurrent open state
aria-controlsGenerated Content/Listbox ID
aria-activedescendantHighlighted Item ID while open
aria-labelExplicit native value
aria-labelledbyExplicit IDs or inherited Field label ID
aria-describedbyExplicit IDs or inherited Field description/error IDs
aria-disabledtrue when disabled
aria-requiredtrue when required
aria-readonlytrue when read only
aria-invalidtrue when invalid
Data attributeValues
[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.

PropTypeDefault
placeholderReactNode-

ARIA: Value adds no ARIA attributes; Trigger owns the control's name and value relationship.

Data attributeValues
[data-slot]"select-value"
[data-placeholder]Present when no selected label is available

Icon

Provides a decorative span for a consumer-supplied trigger icon.

PropTypeDefault
Props: Icon has no Atom-owned behavior props and accepts native span
props.
ARIA attributeValues
aria-hiddenAlways true
Data attributeValues
[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.

PropTypeDefault
containerHTMLElement | nulldocument.body after mount
disabledbooleanfalse

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.

PropTypeDefault
disablePortalbooleanfalse
containerHTMLElement | nulldocument.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 attributeValues
role"listbox"
aria-labelNative value when provided
Data attributeValues
[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 attributeValues
aria-hiddenAlways true
Data attributeValues
[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 attributeValues
[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 attributeValues
role"group"
aria-labelledbyGenerated Label ID
Data attributeValues
[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 attributeValues
[data-slot]"select-label"

Item

Registers one value and text label, owns pointer highlighting and selection, and returns focus to Trigger after selection.

PropTypeDefault
valuestringRequired
disabledbooleanfalse
labelstring-
ARIA attributeValues
role"option"
aria-selectedtrue when selected
aria-disabledtrue when disabled
aria-labelledbyItemText ID when ItemText is mounted
Data attributeValues
[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 attributeValues
[data-slot]"select-item-text"

ItemIndicator

Renders a decorative selected marker only for the current Item unless forceMount keeps it mounted for stable composition.

PropTypeDefault
forceMountbooleanfalse
ARIA attributeValues
aria-hiddenAlways true
Data attributeValues
[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 attributeValues
role"separator"
aria-orientation"horizontal"
Data attributeValues
[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 attributeValues
aria-hiddenAlways true
Data attributeValues
[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 attributeValues
aria-hiddenAlways true
Data attributeValues
[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.

PropTypeDefault
disablePortalbooleanfalse
containerHTMLElement | nulldocument.body after mount
ARIA attributeValues
role"listbox"
aria-labelNative value when provided
Data attributeValues
[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

tsx
import { Field, Select } from "@flowstack-ui/atom";

export default function PlanSelect() {
  return (
    <Field.Root id="plan" required>
      <Field.Label>Plan</Field.Label>
      <Select.Root name="plan" defaultValue="pro">
        <Select.Trigger>
          <Select.Value placeholder="Choose a plan" />
          <Select.Icon>Open</Select.Icon>
        </Select.Trigger>
        <Select.Content>
          <Select.Viewport>
            <Select.Item value="starter">
              <Select.ItemText>Starter</Select.ItemText>
            </Select.Item>
            <Select.Item value="pro">
              <Select.ItemText>Pro</Select.ItemText>
              <Select.ItemIndicator>Selected</Select.ItemIndicator>
            </Select.Item>
          </Select.Viewport>
        </Select.Content>
      </Select.Root>
    </Field.Root>
  );
}

Grouped Options

tsx
import { Select } from "@flowstack-ui/atom";

export default function GroupedSelect() {
  return (
    <Select.Root>
      <Select.Trigger aria-label="Choose a plan">
        <Select.Value placeholder="Choose a plan" />
      </Select.Trigger>
      <Select.Content>
        <Select.Group>
          <Select.Label>Plans</Select.Label>
          <Select.Item value="team">
            <Select.ItemText>Team</Select.ItemText>
          </Select.Item>
          <Select.Item value="enterprise">
            <Select.ItemText>Enterprise</Select.ItemText>
          </Select.Item>
        </Select.Group>
      </Select.Content>
    </Select.Root>
  );
}

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.

KeyDescription
ArrowDownOpens the listbox or moves to the next enabled item.
ArrowUpOpens the listbox from the last item or moves to the previous enabled item.
EnterOpens the listbox or selects the highlighted item.
SpaceOpens the listbox or selects the highlighted item.
HomeHighlights the first enabled item.
EndHighlights the last enabled item.
Printable characterTypeahead search.
EscapeCloses the listbox.
TabCloses 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.onInteractOutside and 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-side and data-align on 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-slot pass-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 asChild composition so the trigger does not render a nested copy of its child.
  • Fixed keyboard opening so ArrowDown, ArrowUp, Home, End, Enter, and Space apply the intended initial highlight after portalled listbox items mount, keeping aria-activedescendant in 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.Trigger inherits Field labels and descriptions while Select.Root inherits Field disabled and required state.
  • Refined Select.Trigger keyboard handler dependencies to avoid recreating callbacks from the full context object.

0.1.0

  • Initial Atom release.