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#

# One-time setup (tokens + lib helpers)npx koalaui-cli@latest init# Add this component (its dependencies come along)npx koalaui-cli@latest add select

Usage#

usage.tsxTSX
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.

AAva Smith · Design lead
IIsaac Reed · Engineering
NNora Lewis · Product
JJames Brennan · Engineering
GGabriel Torres · Support

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.

PropTypeDefaultDescription
valuestring-The selected value (controlled).
defaultValuestring-The value it starts with (uncontrolled).
onValueChange(value: string) => void-Called when an option is picked.
openboolean-Open state of the list (controlled).
defaultOpenboolean-Open state on mount (uncontrolled).
onOpenChange(open: boolean) => void-Called when the list opens or closes.
namestring-Posts the value with a native form submit.
disabledboolean-Disables the whole select.
requiredboolean-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.

PropTypeDefaultDescription
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 attributeDescription
data-stateopen or closed.
data-placeholderPresent while no value is selected; the value reads lighter.
data-disabledPresent when the select is disabled.

SelectValue

Radix Select.Value re-exported: mirrors the chosen item (icon and label) inside the trigger.

PropTypeDefaultDescription
placeholderReact.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.

PropTypeDefaultDescription
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.
sideOffsetnumber6Gap (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 attributeDescription
data-stateopen or closed.
data-sideThe side the list opened on, with popper.
data-alignThe alignment it settled on, with popper.
CSS variableDescription
--radix-select-trigger-widthThe trigger's width; the list is at least this wide.
--radix-select-content-available-heightRoom 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.

PropTypeDefaultDescription
value*string-The value this option commits.
disabledboolean-Skips the option in keyboard navigation and blocks picking it.
textValuestring-Text for type-ahead when the children are not plain text.
tooltipReact.ReactNode-A hint shown on hover and keyboard focus, for terse labels.
tooltipPlacementTooltipProps["placement"]"right"The side the hint grows toward.
density"compact" | "comfortable""compact"Row height. Falls back to the nearest DensityProvider.
Data attributeDescription
data-statechecked on the selected option, else unchecked.
data-highlightedPresent on the option under the pointer or keyboard.
data-disabledPresent 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.

PropTypeDefaultDescription
valuestring-The selected value (controlled).
defaultValuestring-The value it starts with (uncontrolled).
onValueChange(value: string) => void-Called when a row is picked.
openboolean-Open state of the panel (controlled).
defaultOpenbooleanfalseOpen state on mount (uncontrolled).
onOpenChange(open: boolean) => void-Called when the panel opens or closes; closing clears the query.
inlinebooleanfalseRenders 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.
namestring-Renders a hidden input so the value posts with a native form submit.
disabledboolean-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 attributeDescription
data-stateopen or closed.

SelectSearchValue

The value shown in the trigger: the label picked this session, or the placeholder. Forwards all native <span> props.

PropTypeDefaultDescription
placeholderReact.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 attributeDescription
data-placeholderPresent 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.

PropTypeDefaultDescription
align"start" | "center" | "end""start"Alignment against the trigger.
sideOffsetnumber6Gap (px) between trigger and panel.
Data attributeDescription
data-stateopen or closed (always open inline).
data-sideThe side the floating panel opened on.
CSS variableDescription
--radix-popover-trigger-widthThe 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.

PropTypeDefaultDescription
placeholderstring"Search…"Placeholder of the field.
aria-labelstring"Search options"Accessible name of the field.
classNamestring-Classes for the wrapper that insets the field.
inputClassNamestring-Classes for the field itself.

SelectSearchList

The scrolling role="listbox" that holds the rows. Forwards all native <div> props.

PropTypeDefaultDescription
labelstring"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.

PropTypeDefaultDescription
value*string-The value this row commits.
textValuestring-What the search matches against; defaults to value, so give it the visible label.
keywordsstring[]-Extra terms that also match the row: a handle, a synonym, a code.
disabledboolean-Skips the row in navigation and blocks picking it.
onSelect(value: string) => void-Called before the row commits.
Data attributeDescription
data-statechecked on the selected row, else unchecked.
data-activetrue on the highlighted row, shared by pointer and keyboard.
data-valueThe row's value.
data-disabledPresent 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.

FAQ#

Use `Select` to pick exactly one value from a short list; it closes on pick. Past a couple of dozen options, `SelectSearch` is the same select with a search field in its panel. For toggling several values with checkboxes or switches that stay open, reach for `MultiSelect`, and for the ~250-country case use the ready-made `CountrySelect` from the Input family.