Select

A dropdown selection component that allows users to choose a single option from a list

Installation

pnpm add @wandercom/design-system-web

Usage

25 lines
import {
  Select,
  SelectContent,
  SelectGroup,
  SelectItem,
  SelectLabel,
  SelectSeparator,
  SelectTrigger,
  SelectValue,
} from '@wandercom/design-system-web/ui/select';

export function Example() {
  return (
    <Select>
      <SelectTrigger>
        <SelectValue placeholder="Select an option" />
      </SelectTrigger>
      <SelectContent>
        <SelectItem value="option-1">Option 1</SelectItem>
        <SelectItem value="option-2">Option 2</SelectItem>
        <SelectItem value="option-3">Option 3</SelectItem>
      </SelectContent>
    </Select>
  );
}

Examples

The default example shows both size variants (default and sm), the disabled state, and the invalid state using aria-invalid.

Loading example...
44 lines
<Select>
  <SelectTrigger className="w-[200px]">
    <SelectValue placeholder="Choose an option" />
  </SelectTrigger>
  <SelectContent>
    <SelectItem value="1">Option 1</SelectItem>
    <SelectItem value="2">Option 2</SelectItem>
    <SelectItem value="3">Option 3</SelectItem>
  </SelectContent>
</Select>

<Select>
  <SelectTrigger size="sm" className="w-[200px]">
    <SelectValue placeholder="Small select" />
  </SelectTrigger>
  <SelectContent>
    <SelectItem value="1">Option 1</SelectItem>
    <SelectItem value="2">Option 2</SelectItem>
    <SelectItem value="3">Option 3</SelectItem>
  </SelectContent>
</Select>

<Select disabled>
  <SelectTrigger className="w-[200px]">
    <SelectValue placeholder="Disabled select" />
  </SelectTrigger>
  <SelectContent>
    <SelectItem value="1">Option 1</SelectItem>
    <SelectItem value="2">Option 2</SelectItem>
    <SelectItem value="3">Option 3</SelectItem>
  </SelectContent>
</Select>

<Select defaultValue="invalid">
  <SelectTrigger aria-invalid="true" className="w-[200px]">
    <SelectValue />
  </SelectTrigger>
  <SelectContent>
    <SelectItem value="invalid">Invalid selection</SelectItem>
    <SelectItem value="1">Option 1</SelectItem>
    <SelectItem value="2">Option 2</SelectItem>
    <SelectItem value="3">Option 3</SelectItem>
  </SelectContent>
</Select>

With groups

Use SelectGroup, SelectLabel, and SelectSeparator to organize options into labeled sections.

Loading example...
20 lines
<Select>
  <SelectTrigger className="w-[200px]">
    <SelectValue placeholder="Choose a city" />
  </SelectTrigger>
  <SelectContent>
    <SelectGroup>
      <SelectLabel>East Coast</SelectLabel>
      <SelectItem value="nyc">New York</SelectItem>
      <SelectItem value="bos">Boston</SelectItem>
      <SelectItem value="dc">Washington DC</SelectItem>
    </SelectGroup>
    <SelectSeparator />
    <SelectGroup>
      <SelectLabel>West Coast</SelectLabel>
      <SelectItem value="sf">San Francisco</SelectItem>
      <SelectItem value="la">Los Angeles</SelectItem>
      <SelectItem value="sea">Seattle</SelectItem>
    </SelectGroup>
  </SelectContent>
</Select>

Scrollable

The popup is capped at 500px tall by default. When the options overflow, the list scrolls and chevron indicators appear over the top and bottom edges to show more content is available; hovering an indicator scrolls the list. Set scrollable={false} on SelectContent to let the popup grow to the full available height without indicators.

Loading example...
25 lines
<Select>
  <SelectTrigger className="w-[280px]">
    <SelectValue placeholder="Choose a timezone" />
  </SelectTrigger>
  <SelectContent>
    {timezones.map(({ value, label }) => (
      <SelectItem key={value} value={value}>
        {label}
      </SelectItem>
    ))}
  </SelectContent>
</Select>

<Select>
  <SelectTrigger className="w-[280px]">
    <SelectValue placeholder="Choose a timezone" />
  </SelectTrigger>
  <SelectContent scrollable={false}>
    {timezones.map(({ value, label }) => (
      <SelectItem key={value} value={value}>
        {label}
      </SelectItem>
    ))}
  </SelectContent>
</Select>

Long labels

By default an item grows so long labels wrap onto multiple lines. Pass truncate on SelectItem to pin the row to a single line and ellipsize overflowing text instead. Truncation is intended for plain text labels; keep the default growing behavior for labels that include icons or other elements.

Loading example...
25 lines
<Select>
  <SelectTrigger className="w-[280px]">
    <SelectValue placeholder="Choose a timezone" />
  </SelectTrigger>
  <SelectContent>
    {timezones.map(({ value, label }) => (
      <SelectItem key={value} value={value}>
        {label}
      </SelectItem>
    ))}
  </SelectContent>
</Select>

<Select>
  <SelectTrigger className="w-[280px]">
    <SelectValue placeholder="Choose a timezone" />
  </SelectTrigger>
  <SelectContent>
    {timezones.map(({ value, label }) => (
      <SelectItem key={value} truncate value={value}>
        {label}
      </SelectItem>
    ))}
  </SelectContent>
</Select>

Props

Select

defaultValue?:

string
The default selected value when the component is first rendered.

value?:

string
The controlled selected value.

onValueChange?:

(value: string | null) => void
Callback fired when the selected value changes.

items?:

Record<string, ReactNode> | { value: string; label: ReactNode }[]
Value/label pairs used to resolve the text shown by SelectValue. Derived automatically from SelectItem children when omitted; pass explicitly when items are rendered inside custom components.

disabled?:

boolean
Disables the select when true.

required?:

boolean
Makes the select required in forms.

name?:

string
The name of the select for form submission.

open?:

boolean
The controlled open state of the select.

onOpenChange?:

(open: boolean) => void
Callback fired when the open state changes.

SelectTrigger

size?:

'default' | 'sm'
Size variant for the trigger. Defaults to 'default'.

className?:

string
Additional CSS classes to apply.

SelectContent

scrollable?:

boolean
Caps the popup at 500px tall and shows scroll-overflow chevrons when items overflow. Set to false to grow to the full available height without indicators. Defaults to true.

side?:

'top' | 'right' | 'bottom' | 'left' | 'inline-start' | 'inline-end'
Side of the trigger the popup is placed against. Defaults to 'bottom'.

sideOffset?:

number
Distance in pixels between the trigger and the popup. Defaults to 4.

align?:

'start' | 'center' | 'end'
Alignment of the content relative to the trigger. Defaults to 'center'.

alignOffset?:

number
Additional offset along the alignment axis in pixels. Defaults to 0.

alignItemWithTrigger?:

boolean
Overlaps the trigger so the selected item aligns with the trigger text (macOS-style). Defaults to false.

position?:

'item-aligned' | 'popper'
Legacy positioning strategy kept for backward compatibility; 'item-aligned' maps to alignItemWithTrigger. Defaults to 'popper'.

container?:

HTMLElement | ShadowRoot | RefObject
The element the popup portal is appended to. Defaults to the nearest PortalContainerProvider or the document body.

className?:

string
Additional CSS classes to apply.

SelectItem

value:

string
The unique value of this select item.

truncate?:

boolean
Pins the row to a single line and ellipsizes overflowing label text. By default the row grows so long labels wrap. Intended for plain text labels. Defaults to false.

disabled?:

boolean
Disables this specific select item.

className?:

string
Additional CSS classes to apply.

SelectLabel

className?:

string
Additional CSS classes to apply to the label.

SelectValue

placeholder?:

string
Text to display when no value is selected.

SelectSeparator

className?:

string
Additional CSS classes to apply to the separator.

Accessibility

The Select component is built on the Base UI Select primitive, which provides full WAI-ARIA compliance out of the box.

  • Renders with role="combobox" on the trigger and role="listbox" on the content, following the WAI-ARIA listbox pattern
  • Supports keyboard interaction: Space and Enter open the dropdown, arrow keys navigate options, and Escape closes it
  • Type-ahead support allows users to jump to options by typing characters
  • Focus is indicated with a visible border for keyboard navigation
  • Disabled selects apply aria-disabled and reduce interactivity to communicate their state visually
  • Set aria-invalid="true" on the trigger to indicate validation errors with a destructive border style
Select