Select
Displays a list of options for the user to pick from—triggered by a button. Built for React applications with Next.js integration and TypeScript support.
That dropdown where you pick one option from a list? That's what a select does. But unlike the boring native select, this one actually looks good, works with your keyboard, and doesn't make you want to cry when you try to style it.
Pick your favorite
Clean dropdown with grouped options:
import * as React from "react"import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue,} from "@/components/ui/select"export default function SelectDemo() { return ( <div className="flex justify-center self-start pt-6 w-full" style={{ all: 'revert', display: 'flex', justifyContent: 'center', alignSelf: 'flex-start', paddingTop: '1.5rem', width: '100%', fontSize: '14px', lineHeight: '1.5', letterSpacing: 'normal' }} > <Select> <SelectTrigger className="w-[180px]"> <SelectValue placeholder="Select a fruit" /> </SelectTrigger> <SelectContent> <SelectGroup> <SelectLabel>Fruits</SelectLabel> <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> </SelectGroup> </SelectContent> </Select> </div> )}
"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 asChild> <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,}
Built on Radix UI Select with full keyboard navigation, typeahead search, and proper ARIA support. This free open source React component handles all the complex positioning and focus management while looking exactly how you want.
npx shadcn@latest add select
Why custom selects beat native ones
Native selects are the IE6 of form controls. Custom selects fix everything:
- Actually styleable - Make it match your design, not the OS
- Typeahead search - Start typing to find options
- Grouped options - Organize with labels and sections
- Custom content - Icons, descriptions, whatever you need
- Keyboard navigation - Arrow keys, Enter, Escape, it all works
- Touch friendly - Works great on mobile without that weird picker
Useful select patterns
Country selector
With flags and search for long lists:
"use client"import * as React from "react"import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue,} from "@/components/ui/select"const countries = [ { value: "us", label: "United States", flag: "🇺🇸" }, { value: "ca", label: "Canada", flag: "🇨🇦" }, { value: "gb", label: "United Kingdom", flag: "🇬🇧" }, { value: "de", label: "Germany", flag: "🇩🇪" }, { value: "fr", label: "France", flag: "🇫🇷" }, { value: "it", label: "Italy", flag: "🇮🇹" }, { value: "es", label: "Spain", flag: "🇪🇸" }, { value: "au", label: "Australia", flag: "🇦🇺" }, { value: "jp", label: "Japan", flag: "🇯🇵" }, { value: "kr", label: "South Korea", flag: "🇰🇷" }, { value: "sg", label: "Singapore", flag: "🇸🇬" }, { value: "br", label: "Brazil", flag: "🇧🇷" }, { value: "mx", label: "Mexico", flag: "🇲🇽" },]const regions = { "North America": ["us", "ca", "mx"], "Europe": ["gb", "de", "fr", "it", "es"], "Asia Pacific": ["au", "jp", "kr", "sg"], "South America": ["br"]}export default function SelectCountry() { const [selected, setSelected] = React.useState("") const selectedCountry = countries.find(country => country.value === selected) return ( <div className="flex justify-center self-start pt-6 w-full" style={{ all: 'revert', display: 'flex', justifyContent: 'center', alignSelf: 'flex-start', paddingTop: '1.5rem', width: '100%', fontSize: '14px', lineHeight: '1.5', letterSpacing: 'normal' }} > <Select value={selected} onValueChange={setSelected}> <SelectTrigger className="w-[250px]"> <SelectValue placeholder="Select a country"> {selectedCountry && ( <div className="flex items-center gap-2"> <span>{selectedCountry.flag}</span> <span>{selectedCountry.label}</span> </div> )} </SelectValue> </SelectTrigger> <SelectContent> {Object.entries(regions).map(([region, countryCodes]) => ( <SelectGroup key={region}> <SelectLabel>{region}</SelectLabel> {countryCodes.map((code) => { const country = countries.find(c => c.value === code)! return ( <SelectItem key={code} value={code}> <div className="flex items-center gap-2"> <span>{country.flag}</span> <span>{country.label}</span> </div> </SelectItem> ) })} </SelectGroup> ))} </SelectContent> </Select> </div> )}
"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 asChild> <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,}
Status selector
Visual indicators for different states:
"use client"import * as React from "react"import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue,} from "@/components/ui/select"import { Badge } from "@/components/ui/badge"import { cn } from "@/lib/utils"const statuses = [ { value: "draft", label: "Draft", color: "bg-gray-100 text-gray-800", icon: "⭕" }, { value: "pending", label: "Pending Review", color: "bg-yellow-100 text-yellow-800", icon: "⏳" }, { value: "approved", label: "Approved", color: "bg-green-100 text-green-800", icon: "✅" }, { value: "rejected", label: "Rejected", color: "bg-red-100 text-red-800", icon: "❌" }, { value: "published", label: "Published", color: "bg-blue-100 text-blue-800", icon: "🌐" }, { value: "archived", label: "Archived", color: "bg-gray-100 text-gray-600", icon: "📦" },]export default function SelectStatus() { const [selected, setSelected] = React.useState("draft") const selectedStatus = statuses.find(status => status.value === selected) return ( <div className="flex justify-center self-start pt-6 w-full" style={{ all: 'revert', display: 'flex', justifyContent: 'center', alignSelf: 'flex-start', paddingTop: '1.5rem', width: '100%', fontSize: '14px', lineHeight: '1.5', letterSpacing: 'normal' }} > <div className="w-full max-w-sm space-y-2"> <label className="text-sm font-medium">Document Status</label> <Select value={selected} onValueChange={setSelected}> <SelectTrigger className="w-full"> <SelectValue> {selectedStatus && ( <div className="flex items-center gap-2"> <span>{selectedStatus.icon}</span> <Badge variant="secondary" className={cn("text-xs", selectedStatus.color)}> {selectedStatus.label} </Badge> </div> )} </SelectValue> </SelectTrigger> <SelectContent> {statuses.map((status) => ( <SelectItem key={status.value} value={status.value}> <div className="flex items-center gap-2 w-full"> <span>{status.icon}</span> <Badge variant="secondary" className={cn("text-xs", status.color)}> {status.label} </Badge> </div> </SelectItem> ))} </SelectContent> </Select> <p className="text-xs text-muted-foreground"> Change the status of this document </p> </div> </div> )}
"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 asChild> <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,}
Form with validation
Integrated with react-hook-form and Zod:
"use client"import Link from "next/link"import { zodResolver } from "@hookform/resolvers/zod"import { useForm } from "react-hook-form"import { toast } from "sonner"import { z } from "zod"import { Button } from "@/components/ui/button"import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage,} from "@/components/ui/form"import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue,} from "@/components/ui/select"const FormSchema = z.object({ email: z .string({ required_error: "Please select an email to display.", }) .email(),})export default function SelectForm() { const form = useForm<z.infer<typeof FormSchema>>({ resolver: zodResolver(FormSchema), }) function onSubmit(data: z.infer<typeof FormSchema>) { toast("You submitted the following values", { description: ( <pre className="mt-2 w-[320px] rounded-md bg-neutral-950 p-4"> <code className="text-white">{JSON.stringify(data, null, 2)}</code> </pre> ), }) } return ( <div className="flex justify-center self-start pt-6 w-full" style={{ all: 'revert', display: 'flex', justifyContent: 'center', alignSelf: 'flex-start', paddingTop: '1.5rem', width: '100%', fontSize: '14px', lineHeight: '1.5', letterSpacing: 'normal' }} > <Form {...form}> <form onSubmit={form.handleSubmit(onSubmit)} className="w-2/3 space-y-6"> <FormField control={form.control} name="email" render={({ field }) => ( <FormItem> <FormLabel>Email</FormLabel> <Select onValueChange={field.onChange} defaultValue={field.value}> <FormControl> <SelectTrigger> <SelectValue placeholder="Select a verified email to display" /> </SelectTrigger> </FormControl> <SelectContent> <SelectItem value="[email protected]">[email protected]</SelectItem> <SelectItem value="[email protected]">[email protected]</SelectItem> <SelectItem value="[email protected]">[email protected]</SelectItem> </SelectContent> </Select> <FormDescription> You can manage email addresses in your{" "} <Link href="#" onClick={(e) => e.preventDefault()} className="underline">email settings</Link>. </FormDescription> <FormMessage /> </FormItem> )} /> <Button type="submit">Submit</Button> </form> </Form> </div> )}
import * as React from "react"import { Slot } from "@radix-ui/react-slot"import { cva, type VariantProps } from "class-variance-authority"import { cn } from "@/lib/utils"const buttonVariants = cva( "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", { variants: { variant: { default: "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", destructive: "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", outline: "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", link: "text-primary underline-offset-4 hover:underline", }, size: { default: "h-9 px-4 py-2 has-[>svg]:px-3", sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", lg: "h-10 rounded-md px-6 has-[>svg]:px-4", icon: "size-9", }, }, defaultVariants: { variant: "default", size: "default", }, })function Button({ className, variant, size, asChild = false, ...props}: React.ComponentProps<"button"> & VariantProps<typeof buttonVariants> & { asChild?: boolean }) { const Comp = asChild ? Slot : "button" return ( <Comp data-slot="button" className={cn(buttonVariants({ variant, size, className }))} {...props} /> )}export { Button, buttonVariants }
Multi-column layout
When you need to show more info:
"use client"import * as React from "react"import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue,} from "@/components/ui/select"import { Badge } from "@/components/ui/badge"const teamMembers = [ { value: "sarah", name: "Sarah Chen", role: "Frontend Developer", avatar: "👩💻", status: "available" }, { value: "mike", name: "Mike Johnson", role: "Backend Engineer", avatar: "👨💼", status: "busy" }, { value: "emma", name: "Emma Wilson", role: "UI/UX Designer", avatar: "🎨", status: "available" }, { value: "alex", name: "Alex Rodriguez", role: "DevOps Engineer", avatar: "⚙️", status: "away" }, { value: "lisa", name: "Lisa Park", role: "Product Manager", avatar: "📋", status: "available" }, { value: "david", name: "David Kim", role: "QA Engineer", avatar: "🔍", status: "busy" },]const statusColors = { available: "bg-green-100 text-green-800", busy: "bg-red-100 text-red-800", away: "bg-yellow-100 text-yellow-800"}const departments = { "Engineering": ["sarah", "mike", "alex", "david"], "Design & Product": ["emma", "lisa"]}export default function SelectMultiColumn() { const [selected, setSelected] = React.useState("") const selectedMember = teamMembers.find(member => member.value === selected) return ( <div className="flex justify-center self-start pt-6 w-full" style={{ all: 'revert', display: 'flex', justifyContent: 'center', alignSelf: 'flex-start', paddingTop: '1.5rem', width: '100%', fontSize: '14px', lineHeight: '1.5', letterSpacing: 'normal' }} > <div className="w-full max-w-sm space-y-2"> <label className="text-sm font-medium">Assign to Team Member</label> <Select value={selected} onValueChange={setSelected}> <SelectTrigger className="w-full"> <SelectValue placeholder="Select team member"> {selectedMember && ( <div className="flex items-center gap-3"> <span className="text-lg">{selectedMember.avatar}</span> <div className="flex flex-col items-start"> <span className="font-medium">{selectedMember.name}</span> <span className="text-xs text-muted-foreground">{selectedMember.role}</span> </div> </div> )} </SelectValue> </SelectTrigger> <SelectContent className="w-80"> {Object.entries(departments).map(([dept, memberIds]) => ( <SelectGroup key={dept}> <SelectLabel className="font-semibold">{dept}</SelectLabel> {memberIds.map((id) => { const member = teamMembers.find(m => m.value === id)! return ( <SelectItem key={id} value={id} className="h-auto py-3"> <div className="flex items-center gap-3 w-full"> <span className="text-lg">{member.avatar}</span> <div className="flex-1 flex items-center justify-between"> <div className="flex flex-col items-start"> <span className="font-medium">{member.name}</span> <span className="text-sm text-muted-foreground">{member.role}</span> </div> <Badge variant="secondary" className={`text-xs capitalize ${statusColors[member.status as keyof typeof statusColors]}`} > {member.status} </Badge> </div> </div> </SelectItem> ) })} </SelectGroup> ))} </SelectContent> </Select> <p className="text-xs text-muted-foreground"> Choose a team member to assign this task </p> </div> </div> )}
"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 asChild> <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,}
These patterns solve real problems. Country pickers that don't suck, status selectors with clear visual feedback, forms that validate properly.
Perfect for forms and filters
Works great anywhere users need to pick from a list - forms, filters, settings, preferences. The kind of places where a good select makes the difference between frustration and flow.
Drops right into Next.js projects. Full TypeScript support. Styled with Tailwind CSS to match the shadcn design system.
Built on Radix UI
Uses Radix UI Select under the hood, which handles the hard stuff:
- Positioning magic - Always visible, even near edges
- Focus management - Tab, arrows, everything just works
- Typeahead - Start typing to jump to options
- Portal rendering - No z-index nightmares
- Scroll buttons - For long lists that need them
- RTL support - Works with any text direction
API Reference
Select
The root container that manages select state.
Prop | Type | Default | Description |
---|---|---|---|
defaultValue | string | - | Default selected value for uncontrolled |
value | string | - | Controlled selected value |
onValueChange | (value: string) => void | - | Called when selection changes |
defaultOpen | boolean | false | Start open for uncontrolled |
open | boolean | - | Controlled open state |
onOpenChange | (open: boolean) => void | - | Called when open state changes |
disabled | boolean | false | Disable the select |
name | string | - | Name for form submission |
required | boolean | false | Whether selection is required |
SelectTrigger
The button that opens the select.
Prop | Type | Default | Description |
---|---|---|---|
className | string | - | Additional CSS classes |
asChild | boolean | false | Pass functionality to child |
Data attributes:
[data-state]
: "open" | "closed"[data-disabled]
: Present when disabled[data-placeholder]
: Present when showing placeholder
SelectValue
Displays the selected value or placeholder.
Prop | Type | Default | Description |
---|---|---|---|
placeholder | string | - | Text when nothing selected |
asChild | boolean | false | Pass functionality to child |
SelectContent
The dropdown panel containing options.
Prop | Type | Default | Description |
---|---|---|---|
className | string | - | Additional CSS classes |
position | "item-aligned" | "popper" | "item-aligned" | Positioning mode |
side | "top" | "bottom" | "bottom" | Preferred side (popper mode) |
sideOffset | number | 4 | Distance from trigger |
align | "start" | "center" | "end" | "center" | Alignment with trigger |
SelectItem
Individual option in the select.
Prop | Type | Default | Description |
---|---|---|---|
className | string | - | Additional CSS classes |
value | string | required | Value for this option |
disabled | boolean | false | Disable this option |
textValue | string | - | Text for typeahead if using custom content |
Data attributes:
[data-state]
: "checked" | "unchecked"[data-highlighted]
: Present when focused[data-disabled]
: Present when disabled
SelectGroup
Container for grouped items.
Prop | Type | Default | Description |
---|---|---|---|
className | string | - | Additional CSS classes |
SelectLabel
Label for a group of items.
Prop | Type | Default | Description |
---|---|---|---|
className | string | - | Additional CSS classes |
SelectSeparator
Visual separator between items or groups.
Prop | Type | Default | Description |
---|---|---|---|
className | string | - | Additional CSS classes |
Keyboard Navigation
Key | Action |
---|---|
Space | Open select and focus selected/first item |
Enter | Open select or select focused item |
Arrow Down | Open select or move to next item |
Arrow Up | Open select or move to previous item |
Home | Focus first item |
End | Focus last item |
Escape | Close select |
Page Down | Jump down multiple items |
Page Up | Jump up multiple items |
Any letter | Jump to item starting with letter |
Common Patterns
Pattern | Use Case | Implementation |
---|---|---|
Simple list | Basic selection | Items without groups |
Grouped options | Organized choices | SelectGroup with SelectLabel |
With icons | Visual options | Icons in SelectItem |
Searchable | Long lists | Add search above viewport |
With descriptions | Complex options | Multi-line SelectItem content |
Disabled options | Unavailable choices | disabled prop on SelectItem |
Make selects that don't suck
Keep these in mind when building selects:
- Set a width - Don't let it jump around when selection changes
- Use placeholders - Tell users what they're selecting
- Group related options - Makes scanning easier
- Disable smartly - Gray out what's not available
- Handle long lists - Add search or better grouping
- Test keyboard nav - Should feel smooth with just arrows
- Consider mobile - Touch targets need to be bigger
- Match trigger to content - Width should usually match
Scroll Area
Augments native scroll functionality for custom, cross-browser styling. Built for React applications with Next.js integration and TypeScript support.
Separator
Visually or semantically separates content. Perfect for dividing sections, creating navigation breadcrumbs, or organizing layouts in React applications with Next.js and TypeScript.