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, 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>

API Reference

SelectMenu
PropTypeDefaultRequired
allowsEmptyCollection
boolean
--
autoComplete
string
--
autoFocus
boolean
--
className
string
--
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
--