Shadcn Select
React select for custom dropdown menus with search, grouping, and keyboard navigation. Built with TypeScript and Tailwind CSS for Next.js using Radix UI.
Select component acting up?
Join our Discord community for help from other developers.
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. This shadcn/ui select brings proper dropdown experiences to your React applications.
Select showcase
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 component handles all the complex positioning and focus management while looking exactly how you want. Styled with Tailwind CSS to match your design system instead of fighting browser default styling.
npx shadcn@latest add select
Why custom selects actually beat native ones
Here's the thing—native selects are basically the IE6 of form controls. They look different on every platform, can't be styled consistently, and offer zero customization options. Think about how frustrating it is when your beautifully designed form suddenly has an ugly system dropdown that doesn't match anything else.
Custom selects solve everything that's wrong with native ones. You get consistent styling across all browsers and devices, typeahead search for long lists, grouped options that actually make sense, and the ability to add icons, descriptions, or any content you need. Plus they work perfectly with keyboard navigation and screen readers.
This free shadcn select handles the complex parts—positioning magic, focus management, portal rendering, keyboard navigation—while you focus on creating dropdown experiences that actually enhance your forms. Whether you're building country selectors, status pickers, or preference settings in your Next.js applications, selects that work intuitively improve user experience in your JavaScript projects.
Common select patterns you'll actually use
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 validation:
"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 information:
"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,}
Category filter selector
Organized options with clear grouping:
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,}
Features
This free open source select component includes everything you need:
- TypeScript-first - Full type safety with selection events and state management
- Radix UI powered - Battle-tested positioning, focus management, and accessibility
- Typeahead search - Start typing to jump to options instantly
- Tailwind CSS styled - Customize with utilities, not fighting browser defaults
- Keyboard accessible - Arrow keys, Enter, Escape, Page Up/Down all work perfectly
- Grouped options - Organize long lists with labels and visual separation
- Portal rendering - No z-index nightmares or positioning issues
- Touch optimized - Works great on mobile without weird native pickers
API Reference
Core Components
Component | Purpose | Key Props |
---|---|---|
Select | Root container | value , onValueChange , open , disabled |
SelectTrigger | Button that opens | Styling and interaction props |
SelectContent | Dropdown panel | position , side , align for placement |
SelectItem | Individual option | value , disabled , textValue for search |
Selection Management
Prop | Type | Purpose |
---|---|---|
value | string | Currently selected value |
defaultValue | string | Initial selection for uncontrolled use |
onValueChange | function | Callback when selection changes |
placeholder | string | Text shown when nothing selected |
Content Organization
Component | Use Case | Purpose |
---|---|---|
SelectGroup | Organized options | Container for related items |
SelectLabel | Section headers | Visual grouping with labels |
SelectSeparator | Visual breaks | Divide sections clearly |
Production tips
Set consistent widths and don't let selects jump around. This free shadcn/ui select component adapts to content, but unpredictable width changes frustrate users. This TypeScript component handles the dropdown mechanics—you provide consistent trigger widths that maintain layout stability in your Next.js applications.
Use placeholders that actually help users understand the choice. "Select option" tells users nothing—"Choose your country" or "Pick a status" provides context. This open source shadcn select shows whatever placeholder text you provide—make it descriptive enough that users understand what they're selecting.
Group related options logically and keep scanning easy. Long lists become overwhelming without organization. This JavaScript component supports visual grouping with labels and separators—use them to create logical categories that match how users think about the options.
Handle long option lists with typeahead search and smart defaults. When you have dozens of options, users should be able to type to find what they need quickly. The Tailwind CSS styled component includes built-in typeahead—organize options alphabetically or by usage frequency to make searching intuitive.
Test keyboard navigation thoroughly and make it feel natural. Power users rely on keyboard shortcuts for form efficiency. This component handles arrow keys, Enter, Escape, and typeahead automatically—ensure your option order and grouping support smooth keyboard-only interaction patterns.
Integration with other components
Selects naturally work with Form components for validation and error handling in your React applications. Use Button components for consistent styling when selects appear alongside other form controls.
For complex interfaces, combine selects with Popover components for additional context or Command components when you need more advanced search functionality. This open source pattern creates powerful filtering and selection experiences.
When building data-heavy applications, pair selects with DataTable components for column filtering or Badge components to show selected filter states. Label components ensure proper accessibility associations with select triggers.
For multi-step workflows, use selects with Dialog components for settings panels or Sheet components for mobile-friendly option selection. Your JavaScript application can compose these shadcn components while maintaining consistent interaction patterns across different contexts.
Questions you might have
Shadcn Scroll Area
React scroll area for custom styled scrollbars with native behavior. Built with TypeScript and Tailwind CSS for Next.js using Radix UI.
Shadcn Separator
React separator for visual content dividers and section breaks with horizontal and vertical orientations. Built with TypeScript and Tailwind CSS for Next.js using Radix UI.