SelectMenu is a fully custom select component built on react-aria-components, designed for app-like experiences (e.g. webviews in native apps). Unlike the native Select, it renders an in-page dropdown instead of delegating to the OS picker.

It uses the modern appearance by default with support for white and grey themes, a dark color mode, three sizes (sm, md, lg), and an error state.

Selection is key-based: give each SelectMenu.Item an id, and control the value with selectedKey/defaultSelectedKey and onSelectionChange. Label the menu with aria-label (or aria-labelledby) on the root SelectMenu.

Basic usage

<SelectMenu theme="white" aria-label="Fruit" defaultSelectedKey="banana">
  <SelectMenu.Trigger placeholder="Pick a fruit" />
  <SelectMenu.Content>
    <SelectMenu.Item id="apple">Apple</SelectMenu.Item>
    <SelectMenu.Item id="banana">Banana</SelectMenu.Item>
    <SelectMenu.Item id="cherry">Cherry</SelectMenu.Item>
  </SelectMenu.Content>
</SelectMenu>

With groups

<SelectMenu theme="white" aria-label="Food">
  <SelectMenu.Trigger placeholder="Pick a food" />
  <SelectMenu.Content>
    <SelectMenu.Group>
      <SelectMenu.Label>Fruits</SelectMenu.Label>
      <SelectMenu.Item id="apple">Apple</SelectMenu.Item>
      <SelectMenu.Item id="banana">Banana</SelectMenu.Item>
    </SelectMenu.Group>
    <SelectMenu.Separator />
    <SelectMenu.Group>
      <SelectMenu.Label>Vegetables</SelectMenu.Label>
      <SelectMenu.Item id="carrot">Carrot</SelectMenu.Item>
      <SelectMenu.Item id="broccoli">Broccoli</SelectMenu.Item>
    </SelectMenu.Group>
  </SelectMenu.Content>
</SelectMenu>

Sizes

<div className="flex flex-col gap-4 min-w-70">
  <SelectMenu theme="white" size="sm" aria-label="Small" defaultSelectedKey="apple">
    <SelectMenu.Trigger placeholder="Small" />
    <SelectMenu.Content>
      <SelectMenu.Item id="apple">Apple</SelectMenu.Item>
      <SelectMenu.Item id="banana">Banana</SelectMenu.Item>
    </SelectMenu.Content>
  </SelectMenu>
  <SelectMenu theme="white" size="md" aria-label="Medium" defaultSelectedKey="apple">
    <SelectMenu.Trigger placeholder="Medium" />
    <SelectMenu.Content>
      <SelectMenu.Item id="apple">Apple</SelectMenu.Item>
      <SelectMenu.Item id="banana">Banana</SelectMenu.Item>
    </SelectMenu.Content>
  </SelectMenu>
  <SelectMenu theme="white" size="lg" aria-label="Large" defaultSelectedKey="apple">
    <SelectMenu.Trigger placeholder="Large" />
    <SelectMenu.Content>
      <SelectMenu.Item id="apple">Apple</SelectMenu.Item>
      <SelectMenu.Item id="banana">Banana</SelectMenu.Item>
    </SelectMenu.Content>
  </SelectMenu>
</div>

Disabled items

<SelectMenu theme="white" aria-label="Options">
  <SelectMenu.Trigger placeholder="Pick an option" />
  <SelectMenu.Content>
    <SelectMenu.Item id="enabled">Enabled option</SelectMenu.Item>
    <SelectMenu.Item id="disabled" isDisabled>Disabled option</SelectMenu.Item>
    <SelectMenu.Item id="another">Another option</SelectMenu.Item>
  </SelectMenu.Content>
</SelectMenu>

Matching the trigger width

By default the dropdown floors at the trigger width but grows to fit its content (up to a viewport/400px cap). Pass matchTriggerWidth on SelectMenu.Content to pin it to the trigger width instead — longer option labels wrap to fit rather than widening the menu. Compare the two below (both triggers are w-56):

<div className="flex gap-4 items-start">
  <SelectMenu theme="white" aria-label="Grows to fit" defaultSelectedKey="a" className="w-56">
    <SelectMenu.Trigger placeholder="Default" />
    <SelectMenu.Content>
      <SelectMenu.Item id="a">Apple</SelectMenu.Item>
      <SelectMenu.Item id="b">Pineapple, mango and passionfruit smoothie</SelectMenu.Item>
    </SelectMenu.Content>
  </SelectMenu>
  <SelectMenu theme="white" aria-label="Matches trigger" defaultSelectedKey="a" className="w-56">
    <SelectMenu.Trigger placeholder="matchTriggerWidth" />
    <SelectMenu.Content matchTriggerWidth>
      <SelectMenu.Item id="a">Apple</SelectMenu.Item>
      <SelectMenu.Item id="b">Pineapple, mango and passionfruit smoothie</SelectMenu.Item>
    </SelectMenu.Content>
  </SelectMenu>
</div>

Dark color mode

colorMode="light | dark"

colorMode="dark" recolours the trigger, chevron, dropdown, items, labels and separators for use on dark (grey-1000) surfaces. It works with either theme; the error state keeps its light look.

<SelectMenu colorMode="dark" aria-label="Choose a video" defaultSelectedKey="1" className="w-64">
  <SelectMenu.Trigger />
  <SelectMenu.Content>
    <SelectMenu.Item id="1">Nuclear fission</SelectMenu.Item>
    <SelectMenu.Item id="2">Nuclear fusion</SelectMenu.Item>
  </SelectMenu.Content>
</SelectMenu>

Full-height mobile layouts

While open, the dropdown locks page scroll by setting overflow: hidden on <html>. On iOS Safari this changes the dynamic viewport, so a page sized with 100dvh reflows behind the menu. Pass isNonModal on SelectMenu.Content to skip the scroll lock on those layouts.

API Reference

SelectMenu
PropTypeDefaultRequired
allowsEmptyCollection
boolean
--
autoComplete
string
--
autoFocus
boolean
--
className
string
--
colorMode
"light" | "dark"
light-
defaultOpen
boolean
--
defaultSelectedKey
null | string | number
--
defaultValue
null | string | number
--
disabledKeys
Iterable<Key>
--
form
string
--
isDisabled
boolean
--
isInvalid
boolean
--
isOpen
boolean
--
isRequired
boolean
--
name
string
--
onBlur
(e: FocusEvent<Element, Element>) => void
--
onChange
(value: Key | null) => void
--
onFocus
(e: FocusEvent<Element, Element>) => void
--
onFocusChange
(isFocused: boolean) => void
--
onKeyDown
(e: KeyboardEvent) => void
--
onKeyUp
(e: KeyboardEvent) => void
--
onOpenChange
(isOpen: boolean) => void
--
onSelectionChange
(key: Key | null) => void
--
placeholder
string
'Select an item' (localized)-
render
DOMRenderFunction<"div", SelectRenderProps>
--
selectedKey
null | string | number
--
selectionMode
"single"
'single'-
shouldCloseOnSelect
boolean
--
size
"sm" | "md" | "lg"
md-
slot
null | string
--
state
"error"
--
theme
"white" | "grey"
grey-
validate
(value: Key) => true | ValidationError | null | undefined
--
validationBehavior
"native" | "aria"
'native'-
value
null | string | number
--
SelectMenu.Trigger
PropTypeDefaultRequired
aria-controls
string
--
aria-current
boolean
--
aria-disabled
boolean
--
aria-expanded
boolean
--
aria-haspopup
boolean
--
aria-pressed
boolean
--
autoFocus
boolean
--
className
string | (values: ButtonRenderProps & { defaultClassName: string | undefined; }) => string
'react-aria-Button'-
form
string
--
formAction
string | (formData: FormData) => void | Promise<void>
--
formEncType
string
--
formMethod
string
--
formNoValidate
boolean
--
formTarget
string
--
isDisabled
boolean
--
isPending
boolean
--
name
string
--
onBlur
(e: FocusEvent<Element, Element>) => void
--
onClick
(e: MouseEvent<FocusableElement, MouseEvent>) => void
--
onFocus
(e: FocusEvent<Element, Element>) => void
--
onFocusChange
(isFocused: boolean) => void
--
onHoverChange
(isHovering: boolean) => void
--
onHoverEnd
(e: HoverEvent) => void
--
onHoverStart
(e: HoverEvent) => void
--
onKeyDown
(e: KeyboardEvent) => void
--
onKeyUp
(e: KeyboardEvent) => void
--
onPress
(e: PressEvent) => void
--
onPressChange
(isPressed: boolean) => void
--
onPressEnd
(e: PressEvent) => void
--
onPressStart
(e: PressEvent) => void
--
onPressUp
(e: PressEvent) => void
--
placeholder
string
--
preventFocusOnPress
boolean
--
render
DOMRenderFunction<"button", ButtonRenderProps>
--
slot
null | string
--
style
CSSProperties | (values: ButtonRenderProps & { defaultStyle: CSSProperties; }) => CSSProperties | undefined
--
type
"button" | "submit" | "reset"
'button'-
value
string
--
SelectMenu.Content
PropTypeDefaultRequired
arrowBoundaryOffset
number
0-
arrowRef
RefObject<Element | null>
--
boundaryElement
Element
document.body-
className
string | (values: PopoverRenderProps & { defaultClassName: string | undefined; }) => string
'react-aria-Popover'-
containerPadding
number
12-
crossOffset
number
0-
defaultOpen
boolean
--
getTargetRect
(target: Element) => DOMRect | null | undefined
target.getBoundingClientRect()-
isEntering
boolean
--
isExiting
boolean
--
isKeyboardDismissDisabled
boolean
false-
isNonModal
boolean
--
isOpen
boolean
--
matchTriggerWidth
boolean
-
maxHeight
number
--
offset
number
4-
onOpenChange
(isOpen: boolean) => void
--
placement
"left" | "right" | "top" | "bottom" | "start" | "end" | "bottom left" | "bottom right" | "bottom start" | "bottom end" | "top left" | "top right" | "top start" | "top end" | "left top" | "left bottom" | "start top" | "start bottom" | "right top" | "right bottom" | "end top" | "end bottom"
'bottom'-
render
DOMRenderFunction<"div", PopoverRenderProps>
--
scrollRef
RefObject<Element | null>
overlayRef-
shouldCloseOnInteractOutside
(element: Element) => boolean
--
shouldFlip
boolean
true-
shouldUpdatePosition
boolean
true-
slot
null | string
--
style
CSSProperties | (values: PopoverRenderProps & { defaultStyle: CSSProperties; }) => CSSProperties | undefined
--
trigger
string
--
triggerRef
RefObject<Element | null>
--
UNSTABLE_portalContainer
Element
document.body-
SelectMenu.Item
PropTypeDefaultRequired
aria-label
string
--
className
string | (values: ListBoxItemRenderProps & { defaultClassName: string | undefined; }) => string
'react-aria-ListBoxItem'-
id
string | number
--
isDisabled
boolean
--
onAction
() => void
--
onBlur
(e: FocusEvent<HTMLDivElement, Element>) => void
--
onClick
(e: MouseEvent<FocusableElement, MouseEvent>) => void
--
onFocus
(e: FocusEvent<HTMLDivElement, Element>) => void
--
onFocusChange
(isFocused: boolean) => void
--
onHoverChange
(isHovering: boolean) => void
--
onHoverEnd
(e: HoverEvent) => void
--
onHoverStart
(e: HoverEvent) => void
--
onKeyDown
(e: KeyboardEvent) => void
--
onKeyUp
(e: KeyboardEvent) => void
--
onPress
(e: PressEvent) => void
--
onPressChange
(isPressed: boolean) => void
--
onPressEnd
(e: PressEvent) => void
--
onPressStart
(e: PressEvent) => void
--
onPressUp
(e: PressEvent) => void
--
render
(props: DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement> | DetailedHTMLProps<LinkWithRequiredHref, HTMLAnchorElement>, renderProps: ListBoxItemRenderProps) => ReactElement<...>
--
style
CSSProperties | (values: ListBoxItemRenderProps & { defaultStyle: CSSProperties; }) => CSSProperties | undefined
--
textValue
string
--
value
object
--
SelectMenu.Label
PropTypeDefaultRequired
render
DOMRenderFunction<"header", undefined>
--
SelectMenu.Separator
PropTypeDefaultRequired
className
string
'react-aria-Separator'-
elementType
string
--
orientation
"horizontal" | "vertical"
'horizontal'-
render
DOMRenderFunction<"div" | "hr", undefined>
--
slot
null | string
--
style
CSSProperties
--