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.
Installation
{
"registries": {
"@designrevision": "https://registry.designrevision.com/r/{name}.json"
}
}
Add to your existing components.json. Requires shadcn/ui v2.3+.
Packages
Props
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
= default
'sm' | 'default'
Trigger height preset (on SelectTrigger).
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.
"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`.
Packages
Props
No props documented yet.
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.
Packages
Props
No props documented yet.
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`.
Packages
Props
No props documented yet.
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.
Packages
Props
No props documented yet.
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.
Packages
Props
No props documented yet.
"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.
Packages
Props
No props documented yet.
"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`).
Packages
Props
No props documented yet.
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`.
Packages
Props
No props documented yet.
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>
)
}
The Shadcn Select is the dropdown for picking one value from a list — the canonical shadcn/ui select, a ten-part Radix composition themed entirely with CSS variables. It pairs with a 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 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 React Hook Form: 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 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.