Skip to content
Component installs need the registry setup— the Base UI shadcn project, the @vegastack namespace and the Cloudflare Access service token.
VegaStack Design

Select

A dropdown for choosing one option — trigger with value and chevron, grouped and scrollable popup, full keyboard navigation, and animated enter/exit.

Status
stable
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/select

The 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):

Select — data-slot="select"
SelectContent — data-slot="select-scroll-down" | "select-scroll-up"
SelectGroup — data-slot="select-group"
SelectItem — data-slot="select-item" | "select-item-indicator" | "select-item-text"
SelectLabel — data-slot="select-label"
SelectList — data-slot="select-list"
SelectSeparator — data-slot="select-separator"
SelectTrigger — data-slot="select-icon" | "select-trigger"
SelectValue — data-slot="select-value"

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. Pass items so the trigger shows the selected option's label. Modal by default in VegaStack so the trigger cannot scroll out from under an open listbox; pass modal={false} for a listbox that leaves the page scrollable.
  • SelectTrigger — the button that opens the popup (data-slot="select-trigger", <button>). Owns the size variant, renders the trailing chevron, and flips it open. Token border that re-colors with the ring token on focus.
  • SelectValue — renders the selected option's label, or the placeholder when 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. Pass alignItemWithTrigger={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 through SelectContent's listProps.
  • SelectItem — a single option (data-slot="select-item", role="option"). Shows a trailing check when selected; tints on keyboard/hover highlight and dims when disabled.
  • SelectGroup — groups related items with a label (data-slot="select-group", role="group").
  • SelectLabel — a heading for a SelectGroup, 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.

Select a role to continue.

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 multiple Select to pick several values compactly from a longer list.

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

AttributeValues
data-slot"select"

SelectTrigger

PropTypeDefaultDescription
size"lg" | "md" | "sm"

Data attributes and CSS variables on SelectTrigger

AttributeValues
data-sizemirrors 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

AttributeValues
data-slot"select-value"

SelectContent

PropTypeDefaultDescription
alignAlign'start'Alignment relative to the trigger.
alignItemWithTriggerbooleantrueWhether 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.
sideSide'bottom'Preferred side of the trigger to render against.
sideOffsetnumber4Gap in px between the trigger and the popup.

Data attributes and CSS variables on SelectContent

AttributeValues
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

AttributeValues
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

AttributeValues
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

AttributeValues
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

AttributeValues
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

AttributeValues
data-slot"select-separator"

Accessibility

  • The trigger exposes role="combobox" with aria-haspopup="listbox"; the popup is a role="listbox" of role="option" items — no manual ARIA wiring needed.
  • Selection is announced via aria-selected; the selected option shows a check indicator. Disabled options carry aria-disabled and are skipped by keyboard navigation.
  • Full keyboard support: open/close, arrow navigation, type-ahead, and Esc to dismiss. Focus returns to the trigger on close.
  • On :focus the trigger re-colors its border with the ring token (focus:border-ring/(--alpha-tint-border)) rather than removing the outline; highlighted options use the accent token. aria-invalid (or data-invalid) tints the trigger border destructive for use inside a Field.
KeyAction
Space / EnterOpen the popup, or select the highlighted option and close.
/ Move the highlight between options (opens the popup if closed).
Home / EndJump to the first / last option.
AZType-ahead — jump to the option starting with the typed characters.
EscClose the popup without changing the selection.
ContractStates tested
Behaviourdefault, disabled, error, invalid, open, selected
Accessibilityfocus-visible, invalid, labeled, semantic-html
Visualdefault, hover, focus, disabled, invalid, error, dark

Do / Don't

Do
Pass an items map so the trigger shows the selected option's label, and group long lists with SelectLabel and SelectSeparator.
Don't
Use a Select for boolean on/off (use a Switch), or omit the SelectValue placeholder.

On this page