Skip to the content
BooleanPress UI

Form

Select

Lets people choose one option from a list that opens below the field.

Import

import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue } from "@booleanpress/ui/select"

Usage

Give the field a visible name: a Label whose htmlFor is the trigger's id. The list opens below the field, at least as wide as it, and flips above when there is no room. This differs from stock, where the list covers the field (position="item-aligned"); pass position="item-aligned" to SelectContent to get that back.

import { Label } from "@booleanpress/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@booleanpress/ui/select"

export function MailerSelect() {
  return (
    <>
      <Label htmlFor="mailer">Mailer</Label>
      <Select defaultValue="ses">
        <SelectTrigger id="mailer">
          <SelectValue placeholder="Choose a mailer" />
        </SelectTrigger>
        <SelectContent>
          <SelectItem value="ses">Amazon SES</SelectItem>
          <SelectItem value="postmark">Postmark</SelectItem>
        </SelectContent>
      </Select>
    </>
  )
}

It is uncontrolled with defaultValue, or controlled with value and onValueChange. The value is always a string, and an item's value cannot be an empty string. SelectTrigger is as wide as its content; add className="w-full" to fill the column. Group items with SelectGroup, SelectLabel and SelectSeparator. For a list that people search, use a combobox built from Command and Popover; for a long list of native options on a phone, NativeSelect is lighter.

Examples

Basic

A named field with a preselected option and a placeholder for the empty state.

Groups and long list

Labelled groups with a separator; a list taller than the room scrolls, with scroll buttons at its ends.

Disabled

A disabled select keeps its value and does not open; a disabled item stays in the list and cannot be chosen.

Invalid

aria-invalid on the trigger draws the error border; aria-describedby reads the message with the name.

Accessibility

Semantics
The trigger is a button with role="combobox", aria-expanded and aria-haspopup="listbox". The open list is a role="listbox" of role="option" items, the chosen one with aria-selected="true". Inside a form, a hidden native select carries the value, so it is submitted and autofilled.
Labels
Name every select: a Label with htmlFor set to the trigger's id, or aria-label when no visible text fits. Put an error message in aria-describedby on the trigger.
Focus
The trigger is in the tab order. Opening moves focus to the chosen option, or to the first; closing returns it to the trigger. While the list is open, the rest of the page is hidden from assistive technology and scroll is locked.
Known limits
  • The list is a portal, outside the trigger's container, so CSS that targets the container does not reach it.
  • There is no search box. Typing selects by the first letters of the labels (type-ahead) and nothing else.

Keyboard

Keyboard
KeyBehaviour
EnterOn the trigger, opens the list. In the list, chooses the focused option and closes it.
SpaceOn the trigger, opens the list. In the list, chooses the focused option.
ArrowDownOn the trigger, opens the list. In the list, moves focus to the next option.
ArrowUpOn the trigger, opens the list. In the list, moves focus to the previous option.
HomeIn the list, moves focus to the first option.
EndIn the list, moves focus to the last option.
A–ZType-ahead: moves focus to the next option whose label starts with the letters typed.
EscapeCloses the list without changing the value, and returns focus to the trigger.

API

Select

Renders Radix Select.Root and passes it every other prop.

Select props
PropTypeDefaultDescription
autoCompletestring
defaultOpenbooleanWhether the list starts open.
defaultValuestringThe value it starts with, when it controls itself.
dir"ltr" | "rtl"Reading direction of the list. Defaults to the provider's.
disabledbooleanStops the select from opening and takes it out of the tab order.
formstring
namestringThe field name, for the value the form submits.
onOpenChange((open: boolean) => void)Called when the list opens or closes.
onValueChange((value: string) => void)Called with the new value when an option is chosen.
openbooleanWhether the list is open, when you control it. Pair it with onOpenChange.
requiredbooleanThe form cannot be submitted until a value is chosen.
valuestringThe chosen value, when you control it. Pair it with onValueChange.

SelectContent

Renders Radix Select.Content and passes it every other prop.

SelectContent props
PropTypeDefaultDescription
align"center" | "start" | "end"How the list lines up with the trigger on the cross axis in popper mode: start, center or end.
alignOffsetnumber
arrowPaddingnumber
asChildboolean
avoidCollisionsboolean
collisionBoundaryBoundary | Boundary[]
collisionPaddingnumber | Partial<Record<"left" | "right" | "top" | "bottom", number>>
forceMounttrueUsed to force mounting when more control is needed. Useful when controlling animation with React animation libraries.
hideWhenDetachedboolean
onCloseAutoFocus((event: Event) => void)Event handler called when auto-focusing on close. Can be prevented.
onEscapeKeyDown((event: KeyboardEvent) => void)Event handler called when the escape key is down. Can be prevented.
onPointerDownOutside((event: PointerDownOutsideEvent) => void)Event handler called when the a pointerdown event happens outside of the DismissableLayer. Can be prevented.
position"item-aligned" | "popper"popperpopper (the default here) opens below the field and flips above when there is no room; item-aligned is stock: the list covers the field and aligns the chosen option with it.
side"left" | "right" | "top" | "bottom"Which side of the trigger the list prefers in popper mode. It flips when there is no room.
sideOffsetnumber4Distance in pixels between the trigger and the list in popper mode.
sticky"partial" | "always"
updatePositionStrategy"always" | "optimized"

SelectGroup

Renders Radix Select.Group and passes it every other prop.

SelectGroup props
PropTypeDefaultDescription
asChildbooleanRender the child element instead, with this part's behaviour and classes merged onto it.

SelectItem

Renders Radix Select.Item and passes it every other prop.

SelectItem props
PropTypeDefaultDescription
valuerequiredstringThe value chosen by this item. It cannot be an empty string.
asChildboolean
disabledbooleanThe item cannot be chosen or focused.
textValuestringText used for type-ahead and for the trigger's display, when the children are not plain text.

SelectLabel

Renders Radix Select.Label and passes it every other prop.

SelectLabel props
PropTypeDefaultDescription
asChildboolean

SelectScrollDownButton

Renders Radix Select.ScrollDownButton and passes it every other prop.

SelectScrollDownButton props
PropTypeDefaultDescription
asChildboolean

SelectScrollUpButton

Renders Radix Select.ScrollUpButton and passes it every other prop.

SelectScrollUpButton props
PropTypeDefaultDescription
asChildboolean

SelectSeparator

Renders Radix Select.Separator and passes it every other prop.

SelectSeparator props
PropTypeDefaultDescription
asChildboolean

SelectTrigger

Renders Radix Select.Trigger and passes it every other prop.

SelectTrigger props
PropTypeDefaultDescription
asChildbooleanRender the child element instead, with this part's behaviour and classes merged onto it.
size"default" | "sm"defaultdefault is 36 px high, sm is 32 px.

SelectValue

Renders Radix Select.Value and passes it every other prop.

SelectValue props
PropTypeDefaultDescription
asChildboolean
placeholderReactNodeShown while no value is chosen, in the muted colour.

Every part takes className, merged with its defaults by cn(), and ref, which reaches the element it renders.

Data attributes: data-slot="select" (Select), data-slot="select-content" (SelectContent), data-slot="select-group" (SelectGroup), data-slot="select-item" (SelectItem), data-slot="select-label" (SelectLabel), data-slot="select-scroll-down-button" (SelectScrollDownButton), data-slot="select-scroll-up-button" (SelectScrollUpButton), data-slot="select-separator" (SelectSeparator), data-slot="select-trigger" (SelectTrigger), data-slot="select-value" (SelectValue), and data-size.

Theming

The trigger takes --control for the border (at least 3:1 against the surface), --ring for focus and --destructive for the invalid state. The list uses --popover, --popover-foreground and --accent for the focused option. Its enter and exit motion is set once in theme.css.

The tokens its classes read. Change their values in your theme, and it follows.

Theme tokens
TokenUsed for
--accentbackground
--accent-foregroundtext
--borderbackground
--controlborder
--destructiveborder, focus ring
--inputborder, background
--muted-foregroundtext
--popoverbackground
--popover-foregroundtext
--ringborder, focus ring