Select
Choose one option from a list, presented in a popover.
@garn/ui/selectDefault
Trigger + placeholder + three options.
Sizes
xs · sm · md · lg · xl trigger heights.
Grouped
Grouped options with labels and a separator.
Invalid
aria-invalid — danger edge + danger focus ring (shared field surface).
Disabled
A disabled trigger.
Installation
garn is copy-in — the garn CLI writes this component's source into your project, so you own and can edit it. It resolves any base files and installs the npm dependencies below for you.
npx garn-ui add select- Registry
select- Deps
@radix-ui/react-selectclass-variance-authoritylucide-react- Registry deps
utilsfield-variants
Import
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem, SelectGroup, SelectLabel, SelectSeparator } from "@garn/ui/select";Anatomy
The parts this component is built from — compose them to assemble it.
SelectRequiredState container (Radix Root) — owns value + open. No DOM of its own.
SelectTriggerRequired[data-slot="select-trigger"]The button that opens the listbox; renders the shared field surface (carries the appearance + size variants).
SelectValue[data-slot="select-value"]Renders the selected value, or the placeholder when empty.
SelectContentRequired[data-slot="select-content"]The portaled popover listbox.
SelectItemRequired[data-slot="select-item"]A selectable option. Requires a unique, non-empty value.
SelectGroupGroups related items under a SelectLabel.
SelectLabel[data-slot="select-label"]A heading inside a group (not the field's label).
SelectSeparator[data-slot="select-separator"]A divider between groups.
Props
The component's public props and their types.
appearance- Description
- No description.
- Type
"ghost" | "outline" | "soft"- Default
outline
size- Description
- No description.
- Type
"lg" | "md" | "sm" | "xl" | "xs"- Default
md
Plus 13 inherited native HTML attributes.
Styling
Target these data-slots and states, and remap these tokens, to restyle without forking the component.
[data-slot="select-trigger"]States
| Selector | State | Description |
|---|---|---|
:hover | hover | Trigger edge / fill grows slightly stronger (shared field surface). |
:focus | focus | Trigger border grows less transparent (foreground/40) on any focus; soft/ghost clear their fill. |
[data-state=open] | open | Content is mounted + animated in. |
[data-state=checked] | item-selected | The chosen item shows its check indicator. |
[data-highlighted] | item-highlighted | Keyboard/pointer-focused item (Radix moves focus to it; styled via :focus). |
[aria-invalid=true] | invalid | Trigger gets the danger border (+ danger ring on focus) from the shared field surface, same as Input. |
[disabled] | disabled | Trigger dims and won't open. |
| Property | Token | Tier |
|---|---|---|
triggerHeight | --garn-control-h-md | semantic · air |
triggerPaddingInline | --garn-control-px-md | semantic · air |
itemMinHeight | --garn-control-h-md | semantic · air |
borderColor | --garn-foreground | semantic |
When to use
Pick one option from a medium-to-long list (~5+) where the choices fit in a popover.
Reach for something else when
- A few options where seeing them all helps (use radio-group)
- free text (use input)
- choosing many (Select is single-select — use a command / multi-select pattern).
Overview
Select lets a person pick one option from a list that's too long to show inline but short enough to scan in a popover (roughly 5–15 choices). It's a compound component: Select owns the value and open state, SelectTrigger is the button you see (it renders the shared field surface, so it carries the same appearance × size variants as Input and lines up with the rest of a form), and SelectContent is the portaled listbox of SelectItems — optionally organized with SelectGroup + SelectLabel.
Because it's built on Radix, the keyboard and ARIA model — typeahead, arrow navigation, aria-activedescendant, focus return — comes for free. Reach for Select when the choices are predefined and the user just needs to pick; if they need to search a long list, that's a Combobox, and if there are only two-to-five always-visible options, a RadioGroup reads better.
Guidelines
Give every SelectItem a unique, non-empty value. An empty string collides with the placeholder slot and will throw; use a real key. Keep the visible label short — the trigger truncates, and a wall of long options is hard to scan.
Use a placeholder for the empty state, a label for the name. SelectValue's placeholder says what to pick ("Choose a country"); it is not a substitute for an external label. Wire the field's accessible name with a Label or Form, the same as any other control.
Group only when it earns its keep. SelectGroup + SelectLabel help when options fall into a few clear buckets; for a flat list they add noise. Don't nest a Select inside another popover/menu — if you're tempted, the pattern probably wants a Command palette or a Combobox instead.
Best practices
- Label the trigger; give SelectValue a clear placeholder for the empty state.
- Give each SelectItem a unique, non-empty
value. - Group long lists with SelectGroup + SelectLabel.
- Don't rely on the placeholder as the field's label.
- Don't use a Select for 2–4 options the user benefits from seeing at once — that's radio-group.
- Don't expect multi-select — Select is single-value.
Content guidelines
- Write a placeholder that names the choice (“Select a timezone”), not a generic “Select…”.
- Keep option labels short, parallel, and in a sensible order; group long lists with SelectLabel.
- Label the trigger separately — the placeholder isn't a substitute for a label.
Troubleshooting
Runtime error or an option that can't be selected.
Cause. A SelectItem with an empty value — Radix reserves the empty string to clear the selection.
Fix. Give every SelectItem a unique, non-empty value.
Screen readers announce the field with no name.
Cause. The SelectValue placeholder is not an accessible label.
Fix. Associate a <label> with the trigger, or pass aria-label to SelectTrigger.
Accessibility
- Role
combobox- ARIA APG
- combobox
- Focus
- Trigger border darkens (foreground/40) on focus; keyboard focus adds a 2px neutral ring (foreground/15). Focus moves into the listbox on open and returns to the trigger on close.
Accessibility requirements
Give the SelectTrigger an accessible label (a <label>, or aria-label/aria-labelledby).
when SelectTrigger has no associated <label>, aria-label, or aria-labelledby (the placeholder doesn't count)
Keyboard
| SpaceEnterArrowDownArrowUp | Open the listbox. |
| ArrowUpArrowDownHomeEnd | Move between options. |
| A–Z | Type-ahead to the matching option. |
| Enter | Select the highlighted option and close. |
| Escape | Close without changing the value. |
- Trigger exposes aria-expanded / aria-controls; the selected option has aria-selected.
- The placeholder is NOT an accessible name — label the trigger explicitly.