Select

An accessible dropdown select for choosing one option from a list. Built on Radix UI Select with full keyboard navigation, typeahead, and RTL support.

With label

Grouped options (with separator)

With disabled option

Disabled

Installation

pnpm
pnpm dlx @hex-core/cli add select

Grouped options

Cluster long option lists (timezones, countries) under SelectLabel headings so users can scan by region.

tsx
<Select defaultValue="America/Los_Angeles">
  <SelectTrigger className="w-[260px]">
    <SelectValue placeholder="Select a timezone" />
  </SelectTrigger>
  <SelectContent>
    <SelectGroup>
      <SelectLabel>Americas</SelectLabel>
      <SelectItem value="America/Los_Angeles">Los Angeles (PT)</SelectItem>
      <SelectItem value="America/Denver">Denver (MT)</SelectItem>
      <SelectItem value="America/New_York">New York (ET)</SelectItem>
    </SelectGroup>
    <SelectGroup>
      <SelectLabel>Europe</SelectLabel>
      <SelectItem value="Europe/London">London (GMT)</SelectItem>
      <SelectItem value="Europe/Berlin">Berlin (CET)</SelectItem>
      <SelectItem value="Europe/Athens">Athens (EET)</SelectItem>
    </SelectGroup>
    <SelectGroup>
      <SelectLabel>Asia</SelectLabel>
      <SelectItem value="Asia/Tokyo">Tokyo (JST)</SelectItem>
      <SelectItem value="Asia/Singapore">Singapore (SGT)</SelectItem>
      <SelectItem value="Asia/Kolkata">Kolkata (IST)</SelectItem>
    </SelectGroup>
  </SelectContent>
</Select>

Controlled with onValueChange

Two-way bind the select value to parent state — useful for filter dropdowns that drive a query.

tsx
import { useState } from "react";

type Status = "all" | "open" | "in-progress" | "closed";

export function StatusFilter() {
  const [status, setStatus] = useState<Status>("all");

  return (
    <div className="grid gap-2">
      <Label htmlFor="status">Status</Label>
      <Select value={status} onValueChange={(value) => setStatus(value as Status)}>
        <SelectTrigger id="status" className="w-[200px]">
          <SelectValue placeholder="Filter by status" />
        </SelectTrigger>
        <SelectContent>
          <SelectItem value="all">All issues</SelectItem>
          <SelectItem value="open">Open</SelectItem>
          <SelectItem value="in-progress">In progress</SelectItem>
          <SelectItem value="closed">Closed</SelectItem>
        </SelectContent>
      </Select>
      <p className="text-xs text-muted-foreground">Showing {status === "all" ? "all issues" : `${status} issues`}.</p>
    </div>
  );
}

API Reference

PropTypeDefaultDescription
value
string[Select root prop] Controlled selected value
defaultValue
string[Select root prop] Default selected value for uncontrolled usage
onValueChange
function[Select root prop] Callback on value change: (value: string) => void
disabled
booleanfalse[Select root prop] Disable the entire select
required
booleanfalse[Select root prop] Mark as required for form validation
name
string[Select root prop] Form field name (for native form submission)

AI Guidance

When to use

Use for choosing one option from a known, finite list (<= ~20 items): timezones, categories, roles, country codes. Pair with Label.

When not to use

Don't use for large searchable lists (use Combobox). Don't use for boolean choices (use Switch/Checkbox). Don't use for action menus (use DropdownMenu). Don't use for multi-select (needs a different component).

Common mistakes

  • Missing Label pairing
  • Forgetting SelectValue inside SelectTrigger
  • Using Select when the list is large (use Combobox)
  • Putting non-SelectItem children inside SelectContent

Accessibility

Full keyboard nav: arrow keys, Home, End, typeahead, Escape to close. Radix handles role='combobox' on trigger, role='listbox' on content, aria-selected on items.

Token budget: 800