Select
A dropdown for picking one value from a list. Built on Radix Select for keyboard navigation, type-ahead and a11y, with interruptible enter and exit. Its sibling SelectSearch adds a search field.
Installation#
Usage#
import { Pizza, Hamburger } from "koala-icons"
import {
Select, SelectTrigger, SelectValue, SelectContent,
SelectItem, SelectGroup, SelectLabel, SelectSeparator,
} from "@/components/ui/select"
export function Example() {
return (
<Select>
<SelectTrigger className="w-56">
<SelectValue placeholder="Select a dish" />
</SelectTrigger>
<SelectContent>
<SelectItem value="pizza">
<span className="flex items-center gap-2"><Pizza /> Pizza</span>
</SelectItem>
<SelectItem value="burger">
<span className="flex items-center gap-2"><Hamburger /> Hamburger</span>
</SelectItem>
</SelectContent>
</Select>
)
}Anatomy#
import {
Select,
SelectTrigger,
SelectValue,
SelectContent,
SelectGroup,
SelectLabel,
SelectItem,
SelectSeparator,
} from "@/components/ui/select"
<Select>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel />
<SelectItem value="..." />
</SelectGroup>
<SelectSeparator />
</SelectContent>
</Select>Groups and labels#
Use SelectGroup and SelectLabel to group related options. Add SelectSeparator between groups for visual clarity.
Long lists#
When the options outgrow the available height the menu scrolls, and its top and bottom edges fade softly into the popover, the scroll fade utility, so there’s a clear “more above / below” cue without a scrollbar. Open this and scroll: the leading edge stays crisp, the trailing edge dissolves, and a keyboard-focused row always lands clear of the fade. The list is capped at 20rem or the room left on screen, whichever is smaller, so on a phone it scrolls inside the panel instead of running off the viewport, and the arrows at either end sit over the fade rather than pushing the rows down.
Searching the list#
Past a couple of dozen options, scrolling stops being a way to find anything. SelectSearch is the same select with a search field in its panel: one value, the same trigger, the same rows and the same ink check, filtered as you type. Focus stays in the field the whole time, so typing never stops: arrows move the highlight, Enter commits the active row, Escape closes. Give each row a textValue (what the search reads, since value is an id) and keywords for the terms people actually type - here, a city finds its timezone. Rows that don’t match render nothing, so groups, separators and the empty state collapse on their own.
Pass inline and the panel renders in the page flow under its trigger instead of floating over it, for a picker that belongs to a form or a side panel. In flow it paints no fill and no shadow, just the hairline, because fill is elevation; the trigger stays quiet too, since the search field inside the panel is what holds the focus. This one also shows renderValue: the value is seeded from outside and the rows only exist while the panel is open, so the trigger is told how to draw it.
Aligned to the selected item#
By default the list opens below the trigger, which keeps the trigger’s brand ring in view and matches the pickers built on Popover (CountrySelect, MultiSelect, DatePicker). Pass position="item-aligned" for a short, toolbar-style list: it opens with the current option laid over the trigger, so the eye never moves. Keep it to a handful of options; a long list belongs below the trigger.
Country select#
A plain Select over ~250 countries is a long scroll, so reach for CountrySelect, a searchable picker that looks like a Select trigger but filters as you type and is fully keyboard-navigable. It returns the selected ISO 3166-1 alpha-2 code and lives in the Input family, sharing the canonical country dataset and circular flags with PhoneInput. Inside a Field it wires up label, aria, and error state automatically.
Disabled items#
Option hints#
Some labels are too terse to stand on their own. High and Medium say nothing about the trade-off behind them. Pass a tooltip to SelectItem to attach a hint that surfaces on hover and on keyboard focus, so the choice is informed without crowding the row. Use tooltipPlacement to change the side it grows toward (defaults to right).
Density#
The trigger height comes from size on SelectTrigger - sm/md/lg map to 32/36/40px, the same scale as Button and Input, so a Select lines up with them at a shared size. density no longer sets the trigger height; it picks the default size when you do not pass one (compact → sm, comfortable → md) and tightens the dropdown menu rows. Pass density to SelectContent (or a parent DensityProvider) for the menu spacing.
Selecting multiple values#
Select picks exactly one value and closes on pick. For checkboxes, switches, and sections that stay open while you toggle several values, reach for Multi Select - a Popover-backed sibling with the same surface and motion.
API reference#
Select
The root, Radix Select.Root re-exported as is; it renders no element. Every part below that renders one accepts className, merged last.
| Prop | Type | Default | Description |
|---|---|---|---|
| value | string | - | The selected value (controlled). |
| defaultValue | string | - | The value it starts with (uncontrolled). |
| onValueChange | (value: string) => void | - | Called when an option is picked. |
| open | boolean | - | Open state of the list (controlled). |
| defaultOpen | boolean | - | Open state on mount (uncontrolled). |
| onOpenChange | (open: boolean) => void | - | Called when the list opens or closes. |
| name | string | - | Posts the value with a native form submit. |
| disabled | boolean | - | Disables the whole select. |
| required | boolean | - | Marks the value as required in a native form. |
SelectTrigger
The button that opens the list, on Radix Select.Trigger with a turning caret appended. Inside a Field it takes the field's id, description and error state. Forwards every Radix trigger prop.
| Prop | Type | Default | Description |
|---|---|---|---|
| size | "sm" | "md" | "lg" | - | Height: 32, 36 or 40px, the scale Button and Input share. Without it, a Form imposes its size, else density picks sm (compact) or md (comfortable). |
| density | "compact" | "comfortable" | - | Picks the default size when none is passed: compact is sm, comfortable is md. |
| Data attribute | Description |
|---|---|
| data-state | open or closed. |
| data-placeholder | Present while no value is selected; the value reads lighter. |
| data-disabled | Present when the select is disabled. |
SelectValue
Radix Select.Value re-exported: mirrors the chosen item (icon and label) inside the trigger.
| Prop | Type | Default | Description |
|---|---|---|---|
| placeholder | React.ReactNode | - | Shown while nothing is selected. |
SelectContent
The portalled list, on Radix Select.Content, with scroll arrows and a scroll fade; a popper list is capped at 20rem or the room left on screen. Forwards every Radix content prop.
| Prop | Type | Default | Description |
|---|---|---|---|
| position | "popper" | "item-aligned" | "popper" | popper opens below the trigger; item-aligned lays the current option over the trigger, for short lists. Koala's default differs from Radix's. |
| sideOffset | number | 6 | Gap (px) between trigger and list, with popper. |
| side | "top" | "right" | "bottom" | "left" | "bottom" | Preferred side, with popper. |
| align | "start" | "center" | "end" | "start" | Alignment against the trigger, with popper. |
| density | "compact" | "comfortable" | "compact" | Row height of the list. Falls back to the nearest DensityProvider. |
| Data attribute | Description |
|---|---|
| data-state | open or closed. |
| data-side | The side the list opened on, with popper. |
| data-align | The alignment it settled on, with popper. |
| CSS variable | Description |
|---|---|
| --radix-select-trigger-width | The trigger's width; the list is at least this wide. |
| --radix-select-content-available-height | Room left on screen; caps the popper list's height. |
SelectItem
An option, on Radix Select.Item, with an ink check on the right; children are a leading icon plus label. Forwards every Radix item prop.
| Prop | Type | Default | Description |
|---|---|---|---|
| value* | string | - | The value this option commits. |
| disabled | boolean | - | Skips the option in keyboard navigation and blocks picking it. |
| textValue | string | - | Text for type-ahead when the children are not plain text. |
| tooltip | React.ReactNode | - | A hint shown on hover and keyboard focus, for terse labels. |
| tooltipPlacement | TooltipProps["placement"] | "right" | The side the hint grows toward. |
| density | "compact" | "comfortable" | "compact" | Row height. Falls back to the nearest DensityProvider. |
| Data attribute | Description |
|---|---|
| data-state | checked on the selected option, else unchecked. |
| data-highlighted | Present on the option under the pointer or keyboard. |
| data-disabled | Present on a disabled option. |
SelectGroup
Radix Select.Group re-exported: groups related options under a SelectLabel.
SelectLabel
A muted group heading, on Radix Select.Label. Takes density like SelectItem.
SelectSeparator
A hairline between groups, on Radix Select.Separator. Takes density like SelectItem.
SelectSearch
The searchable sibling, on Radix Popover and the same recipe: one value, a search field in the panel. Renders a <div> only when inline, so className applies only then.
| Prop | Type | Default | Description |
|---|---|---|---|
| value | string | - | The selected value (controlled). |
| defaultValue | string | - | The value it starts with (uncontrolled). |
| onValueChange | (value: string) => void | - | Called when a row is picked. |
| open | boolean | - | Open state of the panel (controlled). |
| defaultOpen | boolean | false | Open state on mount (uncontrolled). |
| onOpenChange | (open: boolean) => void | - | Called when the panel opens or closes; closing clears the query. |
| inline | boolean | false | Renders the panel in the page flow under the trigger, with no fill or shadow; picking keeps it open and Escape clears the query. |
| filter | (query: string, value: string, keywords?: string[]) => number | - | Scores each row; 0 hides it. The default is a case-insensitive substring match over textValue and keywords. |
| name | string | - | Renders a hidden input so the value posts with a native form submit. |
| disabled | boolean | - | Disables the trigger. Falls back to the enclosing Field's disabled state. |
| size | "sm" | "md" | "lg" | - | Trigger height, resolved like SelectTrigger's. |
| density | "compact" | "comfortable" | "compact" | Row height and default trigger size. Falls back to the nearest DensityProvider. |
SelectSearchTrigger
The trigger <button>, styled like SelectTrigger; a leading icon may sit before the value. Inside a Field it takes the field's wiring. Forwards all native <button> props except type.
| Data attribute | Description |
|---|---|
| data-state | open or closed. |
SelectSearchValue
The value shown in the trigger: the label picked this session, or the placeholder. Forwards all native <span> props.
| Prop | Type | Default | Description |
|---|---|---|---|
| placeholder | React.ReactNode | - | Shown while nothing is selected. |
| renderValue | (value: string) => React.ReactNode | - | Draws the value yourself; needed when it is seeded from outside, since rows only exist while the panel is open. |
| Data attribute | Description |
|---|---|
| data-placeholder | Present while nothing is selected. |
SelectSearchContent
The panel: Radix Popover.Content in a portal, or a plain <div> when inline. Its width matches the trigger. Forwards the Popover content props.
| Prop | Type | Default | Description |
|---|---|---|---|
| align | "start" | "center" | "end" | "start" | Alignment against the trigger. |
| sideOffset | number | 6 | Gap (px) between trigger and panel. |
| Data attribute | Description |
|---|---|
| data-state | open or closed (always open inline). |
| data-side | The side the floating panel opened on. |
| CSS variable | Description |
|---|---|
| --radix-popover-trigger-width | The trigger's width; the floating panel matches it. |
SelectSearchInput
The search field, a role="combobox" Input that keeps focus while the arrows rove the rows. Forwards native <input> props except value, onChange and size.
| Prop | Type | Default | Description |
|---|---|---|---|
| placeholder | string | "Search…" | Placeholder of the field. |
| aria-label | string | "Search options" | Accessible name of the field. |
| className | string | - | Classes for the wrapper that insets the field. |
| inputClassName | string | - | Classes for the field itself. |
SelectSearchList
The scrolling role="listbox" that holds the rows. Forwards all native <div> props.
| Prop | Type | Default | Description |
|---|---|---|---|
| label | string | "Options" | Accessible name of the list. |
SelectSearchItem
A row; one that does not match the query renders nothing. Forwards all native <div> props except onSelect.
| Prop | Type | Default | Description |
|---|---|---|---|
| value* | string | - | The value this row commits. |
| textValue | string | - | What the search matches against; defaults to value, so give it the visible label. |
| keywords | string[] | - | Extra terms that also match the row: a handle, a synonym, a code. |
| disabled | boolean | - | Skips the row in navigation and blocks picking it. |
| onSelect | (value: string) => void | - | Called before the row commits. |
| Data attribute | Description |
|---|---|
| data-state | checked on the selected row, else unchecked. |
| data-active | true on the highlighted row, shared by pointer and keyboard. |
| data-value | The row's value. |
| data-disabled | Present on a disabled row. |
SelectSearchGroup
A role="group" that hides, label and all, once every row in it filters out. Forwards all native <div> props.
SelectSearchLabel
A muted group heading. Forwards all native <div> props.
SelectSearchSeparator
A hairline role="separator" between groups. Forwards all native <div> props.
SelectSearchEmpty
Shown only while the list holds no row. Its children default to "No results found.". Forwards all native <div> props.