Select

A fully accessible dropdown select built on Radix UI Select. Supports grouped options, disabled items, controlled and uncontrolled state, custom placeholder text, and a large-list scrollable variant with up/down scroll buttons — suitable for forms, filters, and settings.

Installation

npm install @designforge/ui

Usage

import {
  Select,
  SelectGroup,
  SelectValue,
  SelectTrigger,
  SelectContent,
  SelectLabel,
  SelectItem,
  SelectSeparator,
  SelectScrollUpButton,
  SelectScrollDownButton,
} from "@/components/ui/select"

Examples

Basic Select

A minimal select with a default selected value.

import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"
 
export function BasicSelect() {
  return (
    <Select defaultValue="react">
      <SelectTrigger className="w-[200px]">
        <SelectValue />
      </SelectTrigger>
      <SelectContent>
        <SelectItem value="react">React</SelectItem>
        <SelectItem value="vue">Vue</SelectItem>
        <SelectItem value="svelte">Svelte</SelectItem>
        <SelectItem value="angular">Angular</SelectItem>
      </SelectContent>
    </Select>
  )
}

With Placeholder

Use SelectValue with a placeholder prop to show hint text when no option is selected.

import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"
 
export function SelectWithPlaceholder() {
  return (
    <Select>
      <SelectTrigger className="w-[220px]">
        <SelectValue placeholder="Select a timezone…" />
      </SelectTrigger>
      <SelectContent>
        <SelectItem value="utc">UTC</SelectItem>
        <SelectItem value="ist">IST (UTC+5:30)</SelectItem>
        <SelectItem value="est">EST (UTC-5)</SelectItem>
        <SelectItem value="pst">PST (UTC-8)</SelectItem>
      </SelectContent>
    </Select>
  )
}

Grouped Options

Use SelectGroup and SelectLabel to organise related options under named headings, with SelectSeparator between groups.

import {
  Select,
  SelectContent,
  SelectGroup,
  SelectItem,
  SelectLabel,
  SelectSeparator,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"
 
export function SelectGrouped() {
  return (
    <Select>
      <SelectTrigger className="w-[240px]">
        <SelectValue placeholder="Select a font…" />
      </SelectTrigger>
      <SelectContent>
        <SelectGroup>
          <SelectLabel>Sans-serif</SelectLabel>
          <SelectItem value="inter">Inter</SelectItem>
          <SelectItem value="geist">Geist</SelectItem>
          <SelectItem value="dm-sans">DM Sans</SelectItem>
        </SelectGroup>
        <SelectSeparator />
        <SelectGroup>
          <SelectLabel>Serif</SelectLabel>
          <SelectItem value="lora">Lora</SelectItem>
          <SelectItem value="merriweather">Merriweather</SelectItem>
        </SelectGroup>
        <SelectSeparator />
        <SelectGroup>
          <SelectLabel>Monospace</SelectLabel>
          <SelectItem value="jetbrains-mono">JetBrains Mono</SelectItem>
          <SelectItem value="fira-code">Fira Code</SelectItem>
        </SelectGroup>
      </SelectContent>
    </Select>
  )
}

Disabled Option

Mark individual options as unavailable with the disabled prop on SelectItem.

import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"
 
export function SelectDisabledOption() {
  return (
    <Select defaultValue="monthly">
      <SelectTrigger className="w-[200px]">
        <SelectValue />
      </SelectTrigger>
      <SelectContent>
        <SelectItem value="monthly">Monthly</SelectItem>
        <SelectItem value="quarterly">Quarterly</SelectItem>
        <SelectItem value="yearly">Yearly</SelectItem>
        <SelectItem value="lifetime" disabled>
          Lifetime — sold out
        </SelectItem>
      </SelectContent>
    </Select>
  )
}

Disabled Select

Set disabled on the root Select component to prevent all interaction.

import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"
 
export function SelectDisabled() {
  return (
    <Select defaultValue="pro" disabled>
      <SelectTrigger className="w-[200px]">
        <SelectValue />
      </SelectTrigger>
      <SelectContent>
        <SelectItem value="free">Free</SelectItem>
        <SelectItem value="pro">Pro</SelectItem>
        <SelectItem value="team">Team</SelectItem>
      </SelectContent>
    </Select>
  )
}

Controlled

Manage selected value with React state to synchronise with other parts of the UI.

import { useState } from "react"
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"
 
const THEMES = ["light", "dark", "system"] as const
 
export function ControlledSelect() {
  const [theme, setTheme] = useState<string>("system")
 
  return (
    <div className="space-y-2">
      <Select value={theme} onValueChange={setTheme}>
        <SelectTrigger className="w-[180px]">
          <SelectValue />
        </SelectTrigger>
        <SelectContent>
          {THEMES.map((t) => (
            <SelectItem key={t} value={t} className="capitalize">
              {t}
            </SelectItem>
          ))}
        </SelectContent>
      </Select>
      <p className="text-sm text-muted-foreground">
        Active theme:{" "}
        <span className="font-medium text-foreground capitalize">{theme}</span>
      </p>
    </div>
  )
}

In a Form

Integrate with a submit handler and label for a complete accessible form field.

import { FormEvent, useState } from "react"
import { Button } from "@/components/ui/button"
import { Label } from "@/components/ui/label"
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"
 
export function SelectInForm() {
  const [country, setCountry] = useState("")
  const [submitted, setSubmitted] = useState(false)
 
  function handleSubmit(e: FormEvent) {
    e.preventDefault()
    setSubmitted(true)
  }
 
  return (
    <form onSubmit={handleSubmit} className="space-y-4 w-[280px]">
      <div className="space-y-1.5">
        <Label htmlFor="country-select">Country</Label>
        <Select value={country} onValueChange={setCountry}>
          <SelectTrigger id="country-select">
            <SelectValue placeholder="Select your country…" />
          </SelectTrigger>
          <SelectContent>
            <SelectItem value="in">India</SelectItem>
            <SelectItem value="us">United States</SelectItem>
            <SelectItem value="gb">United Kingdom</SelectItem>
            <SelectItem value="de">Germany</SelectItem>
            <SelectItem value="jp">Japan</SelectItem>
          </SelectContent>
        </Select>
      </div>
      <Button type="submit" disabled={!country}>
        Submit
      </Button>
      {submitted && (
        <p className="text-sm text-muted-foreground">
          Selected:{" "}
          <span className="font-medium text-foreground">
            {country.toUpperCase()}
          </span>
        </p>
      )}
    </form>
  )
}

Large List with Scroll

SelectScrollUpButton and SelectScrollDownButton appear automatically when the list overflows. The default position="popper" constrains height to the viewport.

import {
  Select,
  SelectContent,
  SelectGroup,
  SelectItem,
  SelectLabel,
  SelectScrollDownButton,
  SelectScrollUpButton,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"
 
const TIMEZONES = [
  {
    region: "Americas",
    zones: [
      "America/New_York",
      "America/Chicago",
      "America/Denver",
      "America/Los_Angeles",
      "America/Anchorage",
      "America/Halifax",
      "America/Sao_Paulo",
    ],
  },
  {
    region: "Europe",
    zones: [
      "Europe/London",
      "Europe/Paris",
      "Europe/Berlin",
      "Europe/Madrid",
      "Europe/Rome",
      "Europe/Helsinki",
      "Europe/Moscow",
    ],
  },
  {
    region: "Asia / Pacific",
    zones: [
      "Asia/Kolkata",
      "Asia/Tokyo",
      "Asia/Shanghai",
      "Asia/Singapore",
      "Asia/Dubai",
      "Asia/Seoul",
      "Australia/Sydney",
    ],
  },
]
 
export function SelectLargeList() {
  return (
    <Select>
      <SelectTrigger className="w-[260px]">
        <SelectValue placeholder="Select timezone…" />
      </SelectTrigger>
      <SelectContent>
        <SelectScrollUpButton />
        {TIMEZONES.map(({ region, zones }) => (
          <SelectGroup key={region}>
            <SelectLabel>{region}</SelectLabel>
            {zones.map((tz) => (
              <SelectItem key={tz} value={tz}>
                {tz.replace(/_/g, " ")}
              </SelectItem>
            ))}
          </SelectGroup>
        ))}
        <SelectScrollDownButton />
      </SelectContent>
    </Select>
  )
}

API Reference

Select

The root component that manages open/close and selected-value state.

PropTypeDefaultDescription
valuestringControlled selected value.
defaultValuestringInitial selected value for uncontrolled usage.
onValueChange(value: string) => voidCallback fired when the selection changes.
disabledbooleanfalseDisables the entire select.
openbooleanControlled open state of the dropdown.
onOpenChange(open: boolean) => voidCallback fired when the dropdown opens or closes.

SelectTrigger

The button that opens the dropdown.

PropTypeDefaultDescription
classNamestringAdditional CSS classes. Use to set a fixed width (e.g., w-[200px]).

SelectValue

Renders the label of the selected item, or placeholder text when nothing is selected.

PropTypeDefaultDescription
placeholderstringText shown when no value is selected.

SelectContent

The floating dropdown panel.

PropTypeDefaultDescription
position"popper" | "fixed""popper"popper anchors to the trigger and constrains height to the viewport. fixed uses fixed CSS positioning.
classNamestringAdditional CSS classes applied to the content panel.

SelectItem

An individual selectable option.

PropTypeDefaultDescription
valuestringRequired. The value submitted when this item is selected.
disabledbooleanfalsePrevents this item from being selected.
classNamestringAdditional CSS classes.

SelectGroup

Groups related SelectItem elements. Should always contain a sibling SelectLabel for an accessible group name.

SelectLabel

A non-interactive heading rendered inside a SelectGroup.

SelectSeparator

A visual horizontal divider between groups or items.

SelectScrollUpButton / SelectScrollDownButton

Scroll affordance buttons that appear automatically when the list content overflows the available height.

Accessibility

  • Built on the WAI-ARIA Listbox pattern via Radix UI; the trigger has role="combobox" and the list has role="listbox".
  • aria-expanded on the trigger reflects open/closed state; aria-activedescendant tracks the focused option.
  • Arrow keys navigate between options; Enter and Space confirm a selection; Escape closes the dropdown without changing the value.
  • Type-ahead: typing characters while the list is open focuses the first matching option.
  • Associate a visible <Label> with the SelectTrigger via matching htmlFor/id so screen readers announce the field name.
  • SelectGroup + SelectLabel produces a labelled option group (role="group" with aria-labelledby) so assistive technologies announce the group heading.
  • Disabled items have aria-disabled="true" and are skipped during keyboard navigation.

Live View

Open in Storybook ↗