Select
Dropdown chooser for short fixed lists - compound primitive built on Radix Select with portaled, animated, and scroll-aware content.
Overview
Select is a compound primitive built on @radix-ui/react-select. The trigger stays in the page flow, while the listbox is portalled to document.body, animated, and scrolls automatically when overflowing. Use it for short fixed lists where the user picks exactly one value.
For lists longer than ~10 items, reach for a Combobox so users can type to filter. For boolean values use Switch or Checkbox. For 2-5 mutually exclusive options where icon affordances make sense, use ToggleGroup with type="single".
Preview
'use client';import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue,} from '@gremorie/rx-forms';export function SelectPreview() { return ( <div className="max-w-xs"> <Select> <SelectTrigger> <SelectValue placeholder="Pick a primitive" /> </SelectTrigger> <SelectContent> <SelectItem value="message">Message</SelectItem> <SelectItem value="conversation">Conversation</SelectItem> <SelectItem value="plan">Plan</SelectItem> <SelectItem value="reasoning">Reasoning</SelectItem> <SelectItem value="tool">Tool</SelectItem> </SelectContent> </Select> </div> );}Anatomy
Select root, owns the value (value / onValueChange)
├─ SelectTrigger the visible button (size = sm | default)
│ └─ SelectValue renders the selected value / placeholder
└─ SelectContent the portalled, scrollable listbox
├─ SelectScrollUpButton scroll-up affordance (auto-included)
├─ SelectGroup grouped section
│ ├─ SelectLabel non-selectable group heading
│ └─ SelectItem one option (check indicator when selected)
├─ SelectSeparator divider between groups
└─ SelectScrollDownButton scroll-down affordance (auto-included)Installation
bash npx gremorie@latest add rx-select bash pnpm dlx gremorie@latest add rx-select bash yarn dlx gremorie@latest add rx-select bash bunx --bun gremorie@latest add rx-select Usage
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@gremorie/rx-forms";
export function Example() {
return (
<Select>
<SelectTrigger>
<SelectValue placeholder="Pick a primitive" />
</SelectTrigger>
<SelectContent>
<SelectItem value="message">Message</SelectItem>
<SelectItem value="reasoning">Reasoning</SelectItem>
<SelectItem value="tool">Tool</SelectItem>
</SelectContent>
</Select>
);
}The Angular edition of this component ships from source today (see the workbench for the side-by-side); its registry entry is coming next.
API
<Select>
Root provider. Owns the selected value and open state.
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | - | Controlled value. |
defaultValue | string | - | Uncontrolled initial value. |
onValueChange | (value: string) => void | - | Fires when the user picks an option. |
open | boolean | - | Controlled open state. |
onOpenChange | (open: boolean) => void | - | Fires when the listbox opens or closes. |
disabled | boolean | false | Disables the trigger. |
name | string | - | Form field name; submits the selected value with surrounding <form>. |
required | boolean | false | Marks the underlying form input as required. |
Forwards to SelectPrimitive.Root.
<SelectTrigger>
The visible button users click to open the listbox.
| Prop | Type | Default | Description |
|---|---|---|---|
size | "sm" | "default" | "default" | sm is h-8; default is h-9. |
Renders the trigger surface and a trailing ChevronDownIcon.
The size variant owns the trigger height via data-[size=...] selectors, so never force a height with className - a manual h-8 on a default-size trigger loses to the variant's h-9. Next to an icon Button, pair size="sm" with size="icon-sm" (both 32px) or the defaults with size="icon" (both 36px). See Action rows and size pairing.
<SelectValue>
| Prop | Type | Default | Description |
|---|---|---|---|
placeholder | ReactNode | - | Rendered when the value is empty. |
Reads from the parent Select context and renders the current value.
<SelectContent>
The portalled listbox. Animates in / out and respects --radix-select-content-available-height.
| Prop | Type | Default | Description |
|---|---|---|---|
position | "item-aligned" | "popper" | "item-aligned" | item-aligned matches the selected item to the trigger; popper floats below the trigger like a Popover. |
align | "start" | "center" | "end" | "center" | Cross-axis alignment of the popover. |
sideOffset | number | 0 | Distance between trigger and content. |
Wraps SelectScrollUpButton + SelectPrimitive.Viewport + SelectScrollDownButton so long lists scroll naturally.
<SelectItem>
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | - | The value reported back to the parent Select. Required. |
disabled | boolean | false | When true, removes the item from the active set. |
Renders a CheckIcon indicator (visible only when selected) and the children as the item text.
<SelectGroup> + <SelectLabel>
Group related items under a non-selectable label. Always render SelectItem inside a SelectGroup.
<SelectSeparator>
Decorative 1px line between groups.
<SelectScrollUpButton> / <SelectScrollDownButton>
Auto-included by SelectContent so you usually don't render them directly. Show up when the listbox is taller than the viewport.
Composition
<Select>is the root context.<SelectTrigger>+<SelectValue>is the visible affordance the user clicks.<SelectContent>is the portalled listbox - wraps a Viewport with scroll buttons.<SelectItem>always goes inside<SelectGroup>- this is how Radix announces grouping to assistive tech. If your list has no logical groups, wrap all items in one default<SelectGroup>anyway.<SelectLabel>names a group;<SelectSeparator>divides groups.
Variations
Simple list
For small fixed lists with no groups.
<Select>
<SelectTrigger>
<SelectValue placeholder="Pick a primitive" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="message">Message</SelectItem>
<SelectItem value="reasoning">Reasoning</SelectItem>
<SelectItem value="tool">Tool</SelectItem>
</SelectGroup>
</SelectContent>
</Select>Grouped with labels and separator
When items fall into distinct buckets, label and separate them so users can scan.
'use client';import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectSeparator, SelectTrigger, SelectValue,} from '@gremorie/rx-forms';export function SelectGroupedPreview() { return ( <div className="max-w-xs"> <Select> <SelectTrigger className="w-48"> <SelectValue placeholder="Pick a timezone" /> </SelectTrigger> <SelectContent> <SelectGroup> <SelectLabel>North America</SelectLabel> <SelectItem value="est">Eastern (EST)</SelectItem> <SelectItem value="cst">Central (CST)</SelectItem> <SelectItem value="pst">Pacific (PST)</SelectItem> </SelectGroup> <SelectSeparator /> <SelectGroup> <SelectLabel>Europe</SelectLabel> <SelectItem value="gmt">Greenwich (GMT)</SelectItem> <SelectItem value="cet">Central European (CET)</SelectItem> </SelectGroup> </SelectContent> </Select> </div> );}Disabled
Disable the whole control with disabled on Select, or lock out a single option with disabled on SelectItem. Disabled items are announced as disabled and skipped by keyboard navigation.
'use client';import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue,} from '@gremorie/rx-forms';export function SelectDisabledPreview() { return ( <div className="flex max-w-xs flex-col gap-3"> <Select disabled> <SelectTrigger> <SelectValue placeholder="Disabled select" /> </SelectTrigger> <SelectContent> <SelectItem value="message">Message</SelectItem> <SelectItem value="conversation">Conversation</SelectItem> </SelectContent> </Select> <Select> <SelectTrigger> <SelectValue placeholder="Some options disabled" /> </SelectTrigger> <SelectContent> <SelectItem value="message">Message</SelectItem> <SelectItem value="conversation" disabled> Conversation (coming soon) </SelectItem> <SelectItem value="plan">Plan</SelectItem> </SelectContent> </Select> </div> );}Controlled with form name
For form submission. name makes the value participate in the surrounding <form> payload.
function ControlledSelect() {
const [model, setModel] = React.useState('opus');
return (
<Select value={model} onValueChange={setModel} name="model">
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="opus">Claude Opus</SelectItem>
<SelectItem value="sonnet">Claude Sonnet</SelectItem>
<SelectItem value="haiku">Claude Haiku</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
);
}Popper position
Switch position="popper" when the listbox should float below the trigger (rather than aligning the selected item with the trigger). Useful for very long lists or when the trigger sits near the top of the page.
<Select>
<SelectTrigger>
<SelectValue placeholder="Choose..." />
</SelectTrigger>
<SelectContent position="popper" sideOffset={4}>
<SelectGroup>
{longList.map((item) => (
<SelectItem key={item.value} value={item.value}>
{item.label}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>Accessibility
- ARIA combobox pattern: Radix wires the full
role="combobox"+aria-expanded+aria-controls+ listbox / option dance. - Keyboard:
Space/Enter/ArrowDownopens the listbox from the trigger.ArrowUp/ArrowDownmoves between items.Home/Endjump to first / last item.- Typing characters performs typeahead search.
Esccloses the listbox without changing the value.
- Focus: trigger gets a 3px focus-visible ring; the open listbox returns focus to the trigger on close.
- Scroll: long lists show scroll-up / scroll-down buttons automatically. Pointing at them auto-scrolls; pressing
Enteror clicking them scrolls in steps. - Disabled items are announced as disabled and skipped by keyboard navigation.
- Portal: the listbox is portalled to
document.body, so it escapes parent overflow clipping without you opting in.
Related
- Button Group - mix
SelectTriggerwithButtons in the same cluster - Toggle Group - icon-led single-select for 2-5 options
- Radio Group - label-led single-select that always shows all options
- Form - wire Select into react-hook-form via
FormField+Controller