Select
A dropdown for choosing one option — trigger with value and chevron, grouped and scrollable popup, full keyboard navigation, and animated enter/exit.
- Status
- Since
0.1.0- Accessibility pattern
- APG select-only combobox
Last updated
Install
Add Select from the VegaStack registry. The CLI verifies the item's integrity hash before writing it.
pnpm dlx shadcn@latest add @vegastack/selectThe same command installs the registry items it composes: @vegastack/floating-surface.
Usage
import {
Select,
SelectTrigger,
SelectValue,
SelectContent,
SelectItem,
} from "@/components/ui/select";
const fonts = { sans: "Sans-serif", serif: "Serif", mono: "Monospace" };
<Select items={fonts}>
<SelectTrigger className="w-56" aria-label="Font family">
<SelectValue placeholder="Select a font" />
</SelectTrigger>
<SelectContent>
<SelectItem value="sans">Sans-serif</SelectItem>
<SelectItem value="serif">Serif</SelectItem>
<SelectItem value="mono">Monospace</SelectItem>
</SelectContent>
</Select>;Pass an items map (or array) to Select so SelectValue can render the label of the selected
option in the trigger instead of its raw value. Control the selection with value +
onValueChange, or leave it uncontrolled with defaultValue. Add the multiple prop to collect
several values at once (see Multiple selection).
Anatomy
Select is a compound component. Every exported part, with the
data-slot it renders (generated from the canonical source):
Examples
Anatomy
Select is a compound component built on Base UI Select.
Compose the parts inside the root:
<Select>
<SelectTrigger>
<SelectValue placeholder="Select an option" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Group heading</SelectLabel>
<SelectItem value="a">Option A</SelectItem>
<SelectItem value="b">Option B</SelectItem>
</SelectGroup>
<SelectSeparator />
<SelectItem value="c">Option C</SelectItem>
</SelectContent>
</Select>Select— the root; owns the value (value/defaultValue/onValueChange) and open state. Doesn't render an element of its own. Passitemsso the trigger shows the selected option's label. Modal by default in VegaStack so the trigger cannot scroll out from under an open listbox; passmodal={false}for a listbox that leaves the page scrollable.SelectTrigger— the button that opens the popup (data-slot="select-trigger",<button>). Owns thesizevariant, renders the trailing chevron, and flips it open. Token border that re-colors with theringtoken on focus.SelectValue— renders the selected option's label, or theplaceholderwhen empty (data-slot="select-value",<span>).SelectContent— the dropdown surface (data-slot="select-content"). Composes Base UI's Portal + Positioner + Popup + List, adds hover scroll arrows for long lists, sizes to at least the trigger width, caps to the viewport height, and animates enter/exit. PassalignItemWithTrigger={false}when the popup edge should align to the trigger edge instead of aligning the selected item text over the trigger value.SelectList— the Base UI list wrapper around items (data-slot="select-list"). Most consumers customize it throughSelectContent'slistProps.SelectItem— a single option (data-slot="select-item",role="option"). Shows a trailing check when selected; tints on keyboard/hover highlight and dims whendisabled.SelectGroup— groups related items with a label (data-slot="select-group",role="group").SelectLabel— a heading for aSelectGroup, auto-associated with it (data-slot="select-label").SelectSeparator— a divider between items or groups (data-slot="select-separator",role="separator").
Groups & separators
Wrap related options in a SelectGroup with a SelectLabel, and divide sections with a
SelectSeparator. Long lists scroll, with hover scroll arrows at the edges; individual options can be
disabled.
Sizes
SelectTrigger takes a size prop: sm (h-7), default (h-8), and lg (h-10) — matching the
input/button scale so selects line up with sibling form controls.
States
A disabled root makes the trigger inert; a list taller than the available viewport height becomes
scrollable with edge arrows.
Invalid
Pass aria-invalid to the SelectTrigger — or wrap the Select in a <Field error>, which wires it
for you — to tint the trigger border destructive (aria-invalid:border-destructive/70 /
data-invalid:border-destructive/70). Pair it with a FieldError so the failure is announced, not
just color-coded.
Multiple selection
Set multiple on the root to let a Select collect several values. value / defaultValue /
onValueChange then work with arrays, and the popup stays open after each pick. Because the trigger
can no longer show a single label, pass a render-function child to SelectValue to summarise the
selection (for example, a comma-joined list with a placeholder when the array is empty).
<Select multiple defaultValue={["bug", "docs"]}>
<SelectTrigger>
<SelectValue>
{(value: string[]) =>
value.length === 0 ? "Select labels" : value.map(labelFor).join(", ")
}
</SelectValue>
</SelectTrigger>
<SelectContent>{/* SelectItems */}</SelectContent>
</Select>Prefer a few always-visible Checkboxes when there are only a handful of options that should stay on screen; reach for a
multipleSelect to pick several values compactly from a longer list.
Popup alignment
By default Base UI aligns the selected item's text over the trigger value, so an open popup can
overlap the trigger. Set alignItemWithTrigger={false} on SelectContent to align the popup edge to
the trigger edge instead — the popup opens below and side / align apply immediately, the more
familiar dropdown behaviour.
Form integration
Pair Select with a Field for a label, description, and error message — Base UI wires the ARIA. Set
name on the root to submit the value with a form:
import { Field } from "@/components/ui/field";
import {
Select,
SelectTrigger,
SelectValue,
SelectContent,
SelectItem,
} from "@/components/ui/select";
<Field label="Timezone" description="Used for scheduling.">
<Select name="timezone" items={{ est: "Eastern", pst: "Pacific" }}>
<SelectTrigger>
<SelectValue placeholder="Select a timezone" />
</SelectTrigger>
<SelectContent>
<SelectItem value="est">Eastern</SelectItem>
<SelectItem value="pst">Pacific</SelectItem>
</SelectContent>
</Select>
</Field>;Playground
Try every trigger size and the disabled state on a small option list, then copy the generated JSX.
const fonts = { sans: "Sans-serif", serif: "Serif", mono: "Monospace" };
<Select items={fonts} defaultValue="serif">
<SelectTrigger aria-label="Font family">
<SelectValue placeholder="Select a font" />
</SelectTrigger>
<SelectContent>
<SelectItem value="sans">Sans-serif</SelectItem>
<SelectItem value="serif">Serif</SelectItem>
<SelectItem value="mono">Monospace</SelectItem>
</SelectContent>
</Select>API Reference
Select
Select adds no props of its own — it accepts everything the underlying element or Base UI primitive accepts (className, ref, ARIA attributes, event handlers).
Data attributes and CSS variables on Select
| Attribute | Values |
|---|---|
data-slot | "select" |
SelectTrigger
| Prop | Type | Default | Description |
|---|---|---|---|
size | "lg" | "md" | "sm" | — |
Data attributes and CSS variables on SelectTrigger
| Attribute | Values |
|---|---|
data-size | mirrors a prop or state value |
data-slot | "select-icon" | "select-trigger" |
SelectValue
SelectValue adds no props of its own — it accepts everything the underlying element or Base UI primitive accepts (className, ref, ARIA attributes, event handlers).
Data attributes and CSS variables on SelectValue
| Attribute | Values |
|---|---|
data-slot | "select-value" |
SelectContent
| Prop | Type | Default | Description |
|---|---|---|---|
align | Align | 'start' | Alignment relative to the trigger. |
alignItemWithTrigger | boolean | true | Whether to align the selected item text over the trigger value. Base UI
enables this by default; set false when you want the popup edge to align
with the trigger instead and for side/align to apply immediately. |
listProps | (Omit<SelectListProps, "ref"> & React.RefAttributes<HTMLDivElement>) | — | Props forwarded to the Base UI Select.List rendered around the options. |
positionerProps | (Omit<SelectPositionerProps, "ref"> & React.RefAttributes<HTMLDivElement>) | — | Props forwarded to the Base UI Select.Positioner. |
side | Side | 'bottom' | Preferred side of the trigger to render against. |
sideOffset | number | 4 | Gap in px between the trigger and the popup. |
Data attributes and CSS variables on SelectContent
| Attribute | Values |
|---|---|
data-slot | "select-scroll-down" | "select-scroll-up" |
SelectList
SelectList adds no props of its own — it accepts everything the underlying element or Base UI primitive accepts (className, ref, ARIA attributes, event handlers).
Data attributes and CSS variables on SelectList
| Attribute | Values |
|---|---|
data-slot | "select-list" |
SelectItem
SelectItem adds no props of its own — it accepts everything the underlying element or Base UI primitive accepts (className, ref, ARIA attributes, event handlers).
Data attributes and CSS variables on SelectItem
| Attribute | Values |
|---|---|
data-slot | "select-item" | "select-item-indicator" | "select-item-text" |
SelectGroup
SelectGroup adds no props of its own — it accepts everything the underlying element or Base UI primitive accepts (className, ref, ARIA attributes, event handlers).
Data attributes and CSS variables on SelectGroup
| Attribute | Values |
|---|---|
data-slot | "select-group" |
SelectLabel
SelectLabel adds no props of its own — it accepts everything the underlying element or Base UI primitive accepts (className, ref, ARIA attributes, event handlers).
Data attributes and CSS variables on SelectLabel
| Attribute | Values |
|---|---|
data-slot | "select-label" |
SelectSeparator
SelectSeparator adds no props of its own — it accepts everything the underlying element or Base UI primitive accepts (className, ref, ARIA attributes, event handlers).
Data attributes and CSS variables on SelectSeparator
| Attribute | Values |
|---|---|
data-slot | "select-separator" |
Accessibility
- The trigger exposes
role="combobox"witharia-haspopup="listbox"; the popup is arole="listbox"ofrole="option"items — no manual ARIA wiring needed. - Selection is announced via
aria-selected; the selected option shows a check indicator. Disabled options carryaria-disabledand are skipped by keyboard navigation. - Full keyboard support: open/close, arrow navigation, type-ahead, and
Escto dismiss. Focus returns to the trigger on close. - On
:focusthe trigger re-colors its border with theringtoken (focus:border-ring/(--alpha-tint-border)) rather than removing the outline; highlighted options use theaccenttoken.aria-invalid(ordata-invalid) tints the trigger border destructive for use inside aField.
| Key | Action |
|---|---|
| Space / Enter | Open the popup, or select the highlighted option and close. |
| ↑ / ↓ | Move the highlight between options (opens the popup if closed). |
| Home / End | Jump to the first / last option. |
| A–Z | Type-ahead — jump to the option starting with the typed characters. |
| Esc | Close the popup without changing the selection. |
| Contract | States tested |
|---|---|
| Behaviour | default, disabled, error, invalid, open, selected |
| Accessibility | focus-visible, invalid, labeled, semantic-html |
| Visual | default, hover, focus, disabled, invalid, error, dark |