Fields and selection
Select
A composable Base UI select with rich items, sizing, and positioning options.
Install
npx shadcn@latest add https://4af.selfsimilar.dev/r/select.jsonUsage
import {
Select,
SelectContent,
SelectItem,
SelectSeparator,
SelectSub,
SelectSubContent,
SelectSubTrigger,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
<Select size="xs" defaultValue="apple">
<SelectTrigger variant="ghost" label="Fruit">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="apple">Apple</SelectItem>
<SelectSeparator />
<SelectSub>
<SelectSubTrigger>More fruit</SelectSubTrigger>
<SelectSubContent>
<SelectItem value="banana">Banana</SelectItem>
</SelectSubContent>
</SelectSub>
</SelectContent>
</Select>API
Main exports: Select, SelectTrigger, SelectSortTrigger, SelectValue, SelectContent,
SelectItem, SelectSeparator, SelectSub, SelectSubTrigger, SelectSubContent
An accessible Base UI single or multi-select with icons declarable on items so a preselected
value resolves before mount, optional in-panel search, a clear footer, content sizing, and the
compact sort/filter trigger.
Trigger variants
SelectTrigger and SelectSortTrigger accept variant="default" | "ghost" | "plain":
The focus ring remains for keyboard users. outline={false} is still accepted as an alias for
variant="plain".
With neither label nor showValue, SelectSortTrigger shows only its glyph in a square box
matching Button's icon sizes: 24, 30, or 34px for xs, sm, or md. A toolbar can switch this
on the selection, so the chip names a filter only once one is set:
<SelectSortTrigger icon={<ListFilter />} showValue={value !== null} aria-label="Sort" />SelectTrigger also accepts an optional label, rendered as a dimmed prefix before the value.
For example, label="Fruit" displays Fruit: Banana.
When SelectContent has no explicit width, it follows the trigger: default and ghost triggers
keep a trigger-width panel, while plain triggers use content width — the longest option label plus
the reserved checkmark slot. Set width="trigger" or width="content" to override that default.
SelectContent opens with a row already active: the current selection (the most recent enabled
one in a multi-select), or the first enabled row when nothing is selected. Typing moves the
active row to the top enabled match. Disabled rows stay visible but are never the initial active
row or the search field's Enter target; when every match is disabled, no row is active. Set
autoFocusFirstItem={false} to opt out for menu-like controls such as a sort trigger.
onValueChange(value, details) can reject a selection with details.cancel(). The trigger,
checked rows, and the selection offered on reopening all retain the accepted value.
<Select defaultValue="active" items={{ active: "Active", paused: "Paused" }}>
<SelectTrigger variant="plain" label="Status">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="active">Active</SelectItem>
<SelectItem value="paused">Paused</SelectItem>
</SelectContent>
</Select>Sizing
The Select family uses the shared control scale: size="xs" is 24px for dense controls,
size="sm" is 30px with compact text and glyphs, and size="md" is 34px for form rows. The
selected trigger and every option row use the same size. The trigger uses a 6px button radius
at xs/sm and 8px at md.
Nested select panels
Use SelectSub to put a second select panel behind a row in SelectContent. The trigger keeps
the chevron after its label. ArrowRight or Enter opens the panel; ArrowLeft or Escape
returns focus to the parent row. Its children are ordinary SelectItems: they use the
same single- or multi-select value, check state, keyboard navigation, and close behavior as rows
in the main panel. When the parent SelectContent is searchable, its field also searches these
nested rows: a matching parent trigger remains in the main panel, and opening it reveals the
matching child rows. Nested panels can be nested again.
In a multi-select, each submenu trigger automatically shows a dimmed count of its selected child items immediately before the chevron.
Nested panels open immediately when their parent row is hovered. Pass a positive delay to
SelectSubTrigger only when a deliberate hover pause is wanted. The initially focused nested row
stays unhighlighted until it is hovered or reached through keyboard navigation.
A hover-opened panel has a safe triangle. Leaving its row with the mouse keeps the panel open
while the pointer travels inside the triangle between the leave point and the panel's near edge,
so a diagonal path across the rows in between doesn't drop it. It closes as soon as the pointer
strays out of the triangle, or once it rests inside for 300ms. closeDelay still applies when
the pointer leaves the panel itself, and for non-mouse pointers.
import {
Select,
SelectContent,
SelectItem,
SelectSub,
SelectSubContent,
SelectSubTrigger,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
<Select defaultValue="apple" items={{ apple: "Apple", banana: "Banana", mango: "Mango" }}>
<SelectTrigger aria-label="Fruit">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="apple">Apple</SelectItem>
<SelectSub>
<SelectSubTrigger>More fruit</SelectSubTrigger>
<SelectSubContent>
<SelectItem value="banana">Banana</SelectItem>
<SelectItem value="mango">Mango</SelectItem>
</SelectSubContent>
</SelectSub>
</SelectContent>
</Select>Separators
Use SelectSeparator directly between option groups. It is backed by Base UI's select
separator, carries the accessible separator role, and is not treated as a selectable row by
keyboard navigation or selection.
import { SelectContent, SelectItem, SelectSeparator } from "@/components/ui/select";
<SelectContent>
<SelectItem value="active">Active</SelectItem>
<SelectSeparator />
<SelectItem value="archived">Archived</SelectItem>
</SelectContent>Sort and filter trigger
SelectSortTrigger ships in select; there is no separate select-sort item. It:
- has a dashed border while no value is selected;
- becomes filled and solid once a value is selected, and while its panel is open;
- shows the selected label or labels from Select context;
- rotates its chevron like the normal Select trigger.
It takes the same variant="ghost" and variant="plain" options as SelectTrigger.
When multiple is enabled, the same trigger becomes a filter control and supports the nested
SelectSub panels described above. Nested rows participate in the multi-selection and keep the
panel open while choices are toggled.
Add clearable to SelectContent for a clear footer: a compact xs ghost reset button with a
SelectSeparator above it. The button is disabled when nothing is selected, and clearing keeps
the panel open. Hovering it moves the active-row highlight from the list to the button.
import {
Select,
SelectContent,
SelectItem,
SelectSortTrigger,
} from "@/components/ui/select";
<Select<string>>
<SelectSortTrigger label="Status" variant="plain" />
<SelectContent clearable autoFocusFirstItem={false}>
<SelectItem value="active">Active</SelectItem>
<SelectItem value="paused">Paused</SelectItem>
</SelectContent>
</Select>footerActions adds buttons for actions on the selection, such as Archive or "Select all". They
share the footer row evenly and take the same hover highlight as clearable. Each takes a
label, an optional leading icon, checked to show a tick, and keepOpen to leave the panel
open. clearable adds its button after them.
indicator={false} hides the checkbox or tick; it does not prevent selection.
<Select<string> multiple value={tags} onValueChange={setTags}>
<SelectSortTrigger label="Tags" variant="plain" />
<SelectContent
searchable
searchPlaceholder="Search tags"
footerActions={[
{ key: "archive", label: "Archive", icon: <Archive />, checked: archived, onSelect: archive },
{ key: "clear", label: "Clear tags", icon: <X />, disabled: tags.length === 0, onSelect: clearTags },
]}
>
{options.map((option) => (
<SelectItem key={option} value={option}>{option}</SelectItem>
))}
</SelectContent>
</Select>Preselected icons
Declare option icons in the root items collection when a closed, preselected trigger must show
its icon on first paint:
<Select
items={[{ value: "sans", label: "Sans-serif", icon: <Type /> }]}
defaultValue="sans"
>SelectItem's own icon only registers once the panel has mounted, so a closed trigger can't
show it on first load. Use iconForValue when the icon is computed rather than declared.
Resolution order is the mounted item, then iconForValue, then items.