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- Source
- select.tsx
- Deps
@radix-ui/react-selectclass-variance-authoritylucide-react- Registry deps
utilsfield-variants
Import
import { Select } from "@garn/ui/select";Props
Generated from the component's TypeScript 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 remap these tokens to restyle without forking the component.
[data-slot="select-trigger"]| 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 |
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.
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.
Enforceable contracts
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.
- Status
- Stable
- Version
v0.3.0- Introduced
v0.1.0
garn doesn't yet record a per-release version history. This reflects the component's current lifecycle state; full changelog entries will appear once the metadata pipeline emits a lifecycle.history field.