# Shadcn Select

> Free shadcn/ui select (dropdown) component for React — grouped & scrollable lists, icons, country picker, React Hook Form validation, controlled value and sizes. Copy or install in one command.

Source: https://designrevision.com/components/select

The **Shadcn Select** is the dropdown for picking **one** value from a list — the canonical <a href="https://ui.shadcn.com/docs/components/select" rel="nofollow">shadcn/ui</a> select, a ten-part <a href="https://www.radix-ui.com/primitives/docs/components/select" rel="nofollow">Radix</a> composition themed entirely with CSS variables. It pairs with a [Label](/components/label) and composes from `SelectTrigger`, `SelectContent`, `SelectItem`, and friends. The examples below cover what you actually build — grouped pickers, icons, country lists, and validated forms.

## When to use — select vs radio vs combobox

Reach for a select when the user picks **one** value and the list is long enough that showing every option inline would crowd the page (roughly 6+). For a handful of always-visible options, a [radio group](/components/radio-group) reads better. When the list is long enough that users need to **filter as they type**, use a combobox. And when they need to choose **several** values, use a multi-select — the select itself is single-choice.

## Grouped and scrollable lists

For long lists, group related options under `SelectLabel` headings inside `SelectGroup`, and divide groups with `SelectSeparator` — exactly the timezone/region pattern. The scroll-up and scroll-down buttons appear automatically when the list is taller than the viewport, and the content is positioned and animated by Radix's popper.

## Controlled value and the empty-value gotcha

The select is controllable with `value` + `onValueChange`, or uncontrolled with `defaultValue`. One sharp edge trips up most developers: a `SelectItem` **cannot** have an empty-string `value` — Radix reserves `""` for the cleared/placeholder state, so `<SelectItem value="">` throws. Give every item a real value; to **clear** the selection, set the `Select` value back to `""` and the placeholder returns. The Controlled example shows this with a clear button.

## React Hook Form

The select drops into <a href="https://react-hook-form.com" rel="nofollow">React Hook Form</a>: read each field's value into `Select value` and write it back with `setValue` (or `field.onChange`) from `onValueChange`. Pair it with a `zod` `enum` to require a choice, and set `aria-invalid` on the `SelectTrigger` to show the error. The React Hook Form example is a complete, working field.

## Accessibility

Radix gives the select full keyboard support — open with Enter or Space, move with the arrow keys, type to jump to an option, and Escape to close — plus the right ARIA roles. Bind the trigger to a [Label](/components/label) with `htmlFor`/`id`, and link any error text with `aria-describedby`. The trigger's placeholder is muted via `data-[placeholder]` so an unfilled select reads as empty.

## Theming

The select reads your design tokens (`--primary`, `--input`, `--ring`, `--popover`), so it follows your theme — including dark mode — with zero overrides. Any palette produced by a shadcn-compatible theme generator applies automatically.

## Installation

### select

**Install:**

```bash
npx shadcn@latest add @designrevision/select
```

**Dependencies:** @radix-ui/react-select, lucide-react, utils

**Props**

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| value | string | — | Controlled selected value (on Select). Note: SelectItem cannot have an empty-string value — set the Select value to "" to clear to the placeholder. |
| defaultValue | string | — | Initial value when uncontrolled (on Select). |
| onValueChange | (value: string) => void | — | Fires when the selection changes (on Select). |
| size | 'sm' \| 'default' | default | Trigger height preset (on SelectTrigger). |

**Usage & accessibility:** Ten composable parts: Select, SelectTrigger, SelectValue, SelectContent, SelectItem, SelectGroup, SelectLabel, SelectSeparator, and SelectScroll{Up,Down}Button. Set aria-invalid on SelectTrigger for the error styling.

```tsx
// components/ui/select.tsx
"use client"

import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"

import { cn } from "@/lib/utils"

function Select({
  ...props
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
  return <SelectPrimitive.Root data-slot="select" {...props} />
}

function SelectGroup({
  ...props
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
  return <SelectPrimitive.Group data-slot="select-group" {...props} />
}

function SelectValue({
  ...props
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
  return <SelectPrimitive.Value data-slot="select-value" {...props} />
}

function SelectTrigger({
  className,
  size = "default",
  children,
  ...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
  size?: "sm" | "default"
}) {
  return (
    <SelectPrimitive.Trigger
      data-slot="select-trigger"
      data-size={size}
      className={cn(
        "border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
        className
      )}
      {...props}
    >
      {children}
      <SelectPrimitive.Icon>
        <ChevronDownIcon className="size-4 opacity-50" />
      </SelectPrimitive.Icon>
    </SelectPrimitive.Trigger>
  )
}

function SelectContent({
  className,
  children,
  position = "popper",
  ...props
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
  return (
    <SelectPrimitive.Portal>
      <SelectPrimitive.Content
        data-slot="select-content"
        className={cn(
          "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
          position === "popper" &&
            "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
          className
        )}
        position={position}
        {...props}
      >
        <SelectScrollUpButton />
        <SelectPrimitive.Viewport
          className={cn(
            "p-1",
            position === "popper" &&
              "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
          )}
        >
          {children}
        </SelectPrimitive.Viewport>
        <SelectScrollDownButton />
      </SelectPrimitive.Content>
    </SelectPrimitive.Portal>
  )
}

function SelectLabel({
  className,
  ...props
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
  return (
    <SelectPrimitive.Label
      data-slot="select-label"
      className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
      {...props}
    />
  )
}

function SelectItem({
  className,
  children,
  ...props
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
  return (
    <SelectPrimitive.Item
      data-slot="select-item"
      className={cn(
        "focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
        className
      )}
      {...props}
    >
      <span className="absolute right-2 flex size-3.5 items-center justify-center">
        <SelectPrimitive.ItemIndicator>
          <CheckIcon className="size-4" />
        </SelectPrimitive.ItemIndicator>
      </span>
      <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
    </SelectPrimitive.Item>
  )
}

function SelectSeparator({
  className,
  ...props
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
  return (
    <SelectPrimitive.Separator
      data-slot="select-separator"
      className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
      {...props}
    />
  )
}

function SelectScrollUpButton({
  className,
  ...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
  return (
    <SelectPrimitive.ScrollUpButton
      data-slot="select-scroll-up-button"
      className={cn(
        "flex cursor-default items-center justify-center py-1",
        className
      )}
      {...props}
    >
      <ChevronUpIcon className="size-4" />
    </SelectPrimitive.ScrollUpButton>
  )
}

function SelectScrollDownButton({
  className,
  ...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
  return (
    <SelectPrimitive.ScrollDownButton
      data-slot="select-scroll-down-button"
      className={cn(
        "flex cursor-default items-center justify-center py-1",
        className
      )}
      {...props}
    >
      <ChevronDownIcon className="size-4" />
    </SelectPrimitive.ScrollDownButton>
  )
}

export {
  Select,
  SelectContent,
  SelectGroup,
  SelectItem,
  SelectLabel,
  SelectScrollDownButton,
  SelectScrollUpButton,
  SelectSeparator,
  SelectTrigger,
  SelectValue,
}
```

## Examples

### With label

A labelled select with a placeholder — the canonical single-choice dropdown built from `SelectTrigger`, `SelectValue`, and `SelectItem`.

**Install:**

```bash
npx shadcn@latest add @designrevision/select-label-01
```

**Dependencies:** @designrevision/select, label

```tsx
// components/ui/select-label-01.tsx
import { Label } from "@/components/ui/label"
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"

export default function SelectLabel01() {
  return (
    <div className="grid w-full max-w-sm gap-2">
      <Label htmlFor="fruit">Favorite fruit</Label>
      <Select>
        <SelectTrigger id="fruit" className="w-full">
          <SelectValue placeholder="Select a fruit" />
        </SelectTrigger>
        <SelectContent>
          <SelectItem value="apple">Apple</SelectItem>
          <SelectItem value="banana">Banana</SelectItem>
          <SelectItem value="blueberry">Blueberry</SelectItem>
          <SelectItem value="grapes">Grapes</SelectItem>
          <SelectItem value="pineapple">Pineapple</SelectItem>
        </SelectContent>
      </Select>
    </div>
  )
}
```

---

### Grouped & scrollable

A timezone picker with `SelectGroup` headings, `SelectSeparator` dividers, and the scroll buttons that appear automatically on long lists.

**Install:**

```bash
npx shadcn@latest add @designrevision/select-groups-01
```

**Dependencies:** @designrevision/select, label

```tsx
// components/ui/select-groups-01.tsx
import * as React from "react"

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

const zones = [
  {
    region: "North America",
    items: [
      ["est", "Eastern (EST)"],
      ["cst", "Central (CST)"],
      ["mst", "Mountain (MST)"],
      ["pst", "Pacific (PST)"],
      ["akst", "Alaska (AKST)"],
      ["hst", "Hawaii (HST)"],
    ],
  },
  {
    region: "Europe & Africa",
    items: [
      ["gmt", "Greenwich Mean (GMT)"],
      ["cet", "Central European (CET)"],
      ["eet", "Eastern European (EET)"],
      ["cat", "Central Africa (CAT)"],
    ],
  },
  {
    region: "Asia",
    items: [
      ["ist", "India (IST)"],
      ["cstc", "China (CST)"],
      ["jst", "Japan (JST)"],
      ["kst", "Korea (KST)"],
    ],
  },
  {
    region: "Australia & Pacific",
    items: [
      ["awst", "Australian Western (AWST)"],
      ["aest", "Australian Eastern (AEST)"],
      ["nzst", "New Zealand (NZST)"],
    ],
  },
]

export default function SelectGroups01() {
  return (
    <div className="grid w-full max-w-sm gap-2">
      <Label htmlFor="timezone">Timezone</Label>
      <Select>
        <SelectTrigger id="timezone" className="w-full">
          <SelectValue placeholder="Select a timezone" />
        </SelectTrigger>
        <SelectContent>
          {zones.map((zone, index) => (
            <React.Fragment key={zone.region}>
              {index > 0 ? <SelectSeparator /> : null}
              <SelectGroup>
                <SelectLabel>{zone.region}</SelectLabel>
                {zone.items.map(([value, label]) => (
                  <SelectItem key={value} value={value}>
                    {label}
                  </SelectItem>
                ))}
              </SelectGroup>
            </React.Fragment>
          ))}
        </SelectContent>
      </Select>
    </div>
  )
}
```

---

### With icons

Status options each with a leading colored dot — the indicator renders in both the open list and the trigger via `SelectValue`.

**Install:**

```bash
npx shadcn@latest add @designrevision/select-icons-01
```

**Dependencies:** @designrevision/select, label

```tsx
// components/ui/select-icons-01.tsx
import { Label } from "@/components/ui/label"
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"

const statuses = [
  { value: "backlog", label: "Backlog", color: "bg-muted-foreground" },
  { value: "todo", label: "Todo", color: "bg-blue-500" },
  { value: "in-progress", label: "In Progress", color: "bg-amber-500" },
  { value: "done", label: "Done", color: "bg-emerald-500" },
  { value: "canceled", label: "Canceled", color: "bg-destructive" },
]

export default function SelectIcons01() {
  return (
    <div className="grid w-full max-w-sm gap-2">
      <Label htmlFor="status">Status</Label>
      <Select defaultValue="in-progress">
        <SelectTrigger id="status" className="w-full">
          <SelectValue placeholder="Select a status" />
        </SelectTrigger>
        <SelectContent>
          {statuses.map((status) => (
            <SelectItem key={status.value} value={status.value}>
              <span className={`size-2 rounded-full ${status.color}`} />
              {status.label}
            </SelectItem>
          ))}
        </SelectContent>
      </Select>
    </div>
  )
}
```

---

### Country picker

A real-world country select with a flag emoji beside each option.

**Install:**

```bash
npx shadcn@latest add @designrevision/select-country-01
```

**Dependencies:** @designrevision/select, label

```tsx
// components/ui/select-country-01.tsx
import { Label } from "@/components/ui/label"
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"

const countries = [
  { value: "us", label: "United States", flag: "🇺🇸" },
  { value: "gb", label: "United Kingdom", flag: "🇬🇧" },
  { value: "ca", label: "Canada", flag: "🇨🇦" },
  { value: "au", label: "Australia", flag: "🇦🇺" },
  { value: "de", label: "Germany", flag: "🇩🇪" },
  { value: "fr", label: "France", flag: "🇫🇷" },
  { value: "jp", label: "Japan", flag: "🇯🇵" },
  { value: "br", label: "Brazil", flag: "🇧🇷" },
]

export default function SelectCountry01() {
  return (
    <div className="grid w-full max-w-sm gap-2">
      <Label htmlFor="country">Country</Label>
      <Select defaultValue="us">
        <SelectTrigger id="country" className="w-full">
          <SelectValue placeholder="Select a country" />
        </SelectTrigger>
        <SelectContent>
          {countries.map((country) => (
            <SelectItem key={country.value} value={country.value}>
              <span className="text-base leading-none">{country.flag}</span>
              {country.label}
            </SelectItem>
          ))}
        </SelectContent>
      </Select>
    </div>
  )
}
```

---

### React Hook Form

A required role select validated with `react-hook-form` and `zod` — the trigger gets `aria-invalid` and an error message until a value is chosen.

**Install:**

```bash
npx shadcn@latest add @designrevision/select-form-01
```

**Dependencies:** react-hook-form, zod, @hookform/resolvers, @designrevision/select, label, button

```tsx
// components/ui/select-form-01.tsx
"use client"

import * as React from "react"
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { z } from "zod"

import { Button } from "@/components/ui/button"
import { Label } from "@/components/ui/label"
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"

const schema = z.object({
  role: z.enum(["admin", "editor", "viewer"], {
    message: "Please select a role.",
  }),
})

type FormValues = z.infer<typeof schema>

export default function SelectForm01() {
  const [submitted, setSubmitted] = React.useState(false)
  const {
    handleSubmit,
    setValue,
    watch,
    formState: { errors },
  } = useForm<FormValues>({ resolver: zodResolver(schema) })

  const role = watch("role")

  return (
    <form
      onSubmit={handleSubmit(() => setSubmitted(true))}
      className="grid w-full max-w-sm gap-4"
    >
      <div className="grid gap-2">
        <Label htmlFor="role">Role</Label>
        <Select
          value={role}
          onValueChange={(value) =>
            setValue("role", value as FormValues["role"], {
              shouldValidate: true,
            })
          }
        >
          <SelectTrigger
            id="role"
            className="w-full"
            aria-invalid={!!errors.role}
            aria-describedby={errors.role ? "role-error" : undefined}
          >
            <SelectValue placeholder="Select a role" />
          </SelectTrigger>
          <SelectContent>
            <SelectItem value="admin">Admin</SelectItem>
            <SelectItem value="editor">Editor</SelectItem>
            <SelectItem value="viewer">Viewer</SelectItem>
          </SelectContent>
        </Select>
        {errors.role ? (
          <p id="role-error" className="text-sm text-destructive">
            {errors.role.message}
          </p>
        ) : null}
      </div>
      <Button type="submit" className="w-fit">
        {submitted ? "Submitted!" : "Save"}
      </Button>
    </form>
  )
}
```

---

### Controlled & clear

A controlled select with a default value and a clear button. Clearing sets the value back to `""`, which shows the placeholder again.

**Install:**

```bash
npx shadcn@latest add @designrevision/select-controlled-01
```

**Dependencies:** lucide-react, @designrevision/select, label, button

```tsx
// components/ui/select-controlled-01.tsx
"use client"

import * as React from "react"
import { X } from "lucide-react"

import { Button } from "@/components/ui/button"
import { Label } from "@/components/ui/label"
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"

const fruits = [
  ["apple", "Apple"],
  ["banana", "Banana"],
  ["blueberry", "Blueberry"],
  ["grapes", "Grapes"],
]

export default function SelectControlled01() {
  const [value, setValue] = React.useState("banana")

  return (
    <div className="grid w-full max-w-sm gap-2">
      <Label htmlFor="fruit-controlled">Favorite fruit</Label>
      <div className="flex items-center gap-2">
        <Select value={value} onValueChange={setValue}>
          <SelectTrigger id="fruit-controlled" className="w-full">
            <SelectValue placeholder="Select a fruit" />
          </SelectTrigger>
          <SelectContent>
            {fruits.map(([itemValue, label]) => (
              <SelectItem key={itemValue} value={itemValue}>
                {label}
              </SelectItem>
            ))}
          </SelectContent>
        </Select>
        <Button
          type="button"
          variant="outline"
          size="icon"
          onClick={() => setValue("")}
          disabled={!value}
          aria-label="Clear selection"
        >
          <X />
        </Button>
      </div>
      <p className="text-sm text-muted-foreground">
        Selected:{" "}
        <span className="font-medium text-foreground">{value || "none"}</span>
      </p>
    </div>
  )
}
```

---

### States

A disabled select, a single disabled option, and an invalid select (`aria-invalid` switches on the destructive ring, paired with `aria-describedby`).

**Install:**

```bash
npx shadcn@latest add @designrevision/select-states-01
```

**Dependencies:** @designrevision/select, label

```tsx
// components/ui/select-states-01.tsx
import { Label } from "@/components/ui/label"
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"

export default function SelectStates01() {
  return (
    <div className="grid w-full max-w-sm gap-6">
      <div className="grid gap-2">
        <Label htmlFor="sel-disabled" className="text-muted-foreground">
          Disabled
        </Label>
        <Select disabled>
          <SelectTrigger id="sel-disabled" className="w-full">
            <SelectValue placeholder="Unavailable" />
          </SelectTrigger>
          <SelectContent>
            <SelectItem value="a">Option A</SelectItem>
          </SelectContent>
        </Select>
      </div>
      <div className="grid gap-2">
        <Label htmlFor="sel-option">One option disabled</Label>
        <Select>
          <SelectTrigger id="sel-option" className="w-full">
            <SelectValue placeholder="Select a plan" />
          </SelectTrigger>
          <SelectContent>
            <SelectItem value="free">Free</SelectItem>
            <SelectItem value="pro">Pro</SelectItem>
            <SelectItem value="enterprise" disabled>
              Enterprise — contact sales
            </SelectItem>
          </SelectContent>
        </Select>
      </div>
      <div className="grid gap-2">
        <Label htmlFor="sel-invalid">Required</Label>
        <Select>
          <SelectTrigger
            id="sel-invalid"
            className="w-full"
            aria-invalid
            aria-describedby="sel-error"
          >
            <SelectValue placeholder="Select an option" />
          </SelectTrigger>
          <SelectContent>
            <SelectItem value="x">Option X</SelectItem>
            <SelectItem value="y">Option Y</SelectItem>
          </SelectContent>
        </Select>
        <p id="sel-error" className="text-sm text-destructive">
          This field is required.
        </p>
      </div>
    </div>
  )
}
```

---

### Sizes

The `sm` and `default` trigger heights via the built-in `size` prop on `SelectTrigger`.

**Install:**

```bash
npx shadcn@latest add @designrevision/select-sizes-01
```

**Dependencies:** @designrevision/select, label

```tsx
// components/ui/select-sizes-01.tsx
import { Label } from "@/components/ui/label"
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"

const items = [
  ["apple", "Apple"],
  ["banana", "Banana"],
  ["grapes", "Grapes"],
]

export default function SelectSizes01() {
  return (
    <div className="flex flex-wrap items-end gap-6">
      <div className="grid gap-2">
        <Label htmlFor="sz-sm" className="text-xs">
          Small
        </Label>
        <Select defaultValue="apple">
          <SelectTrigger id="sz-sm" size="sm" className="w-[140px]">
            <SelectValue />
          </SelectTrigger>
          <SelectContent>
            {items.map(([value, label]) => (
              <SelectItem key={value} value={value}>
                {label}
              </SelectItem>
            ))}
          </SelectContent>
        </Select>
      </div>
      <div className="grid gap-2">
        <Label htmlFor="sz-default">Default</Label>
        <Select defaultValue="apple">
          <SelectTrigger id="sz-default" className="w-[160px]">
            <SelectValue />
          </SelectTrigger>
          <SelectContent>
            {items.map(([value, label]) => (
              <SelectItem key={value} value={value}>
                {label}
              </SelectItem>
            ))}
          </SelectContent>
        </Select>
      </div>
    </div>
  )
}
```
