Combobox
React combobox with search, autocomplete, and keyboard navigation. Perfect for dropdowns with many options, user selection, and searchable interfaces.
Need a dropdown that can handle hundreds of options? Users hate scrolling through long lists. Combobox combines the best of dropdowns and search - click to see options, type to filter instantly. Perfect for countries, frameworks, team members, or any list where search saves time.
import * as React from "react"import { Check, ChevronsUpDown } from "lucide-react"import { cn } from "@/lib/utils"import { Button } from "@/components/ui/button"import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList,} from "@/components/ui/command"import { Popover, PopoverContent, PopoverTrigger,} from "@/components/ui/popover"const frameworks = [ { value: "next.js", label: "Next.js", }, { value: "sveltekit", label: "SvelteKit", }, { value: "nuxt.js", label: "Nuxt.js", }, { value: "remix", label: "Remix", }, { value: "astro", label: "Astro", },]export default function ComboboxDemo() { const [open, setOpen] = React.useState(false) const [value, setValue] = React.useState("") return ( <div className="w-full p-6 flex justify-center"> <Popover open={open} onOpenChange={setOpen}> <PopoverTrigger asChild> <Button variant="outline" role="combobox" aria-expanded={open} className="w-[200px] justify-between" > {value ? frameworks.find((framework) => framework.value === value)?.label : "Select framework..."} <ChevronsUpDown className="opacity-50" /> </Button> </PopoverTrigger> <PopoverContent className="w-[200px] p-0"> <Command> <CommandInput placeholder="Search framework..." className="h-9" /> <CommandList> <CommandEmpty>No framework found.</CommandEmpty> <CommandGroup> {frameworks.map((framework) => ( <CommandItem key={framework.value} value={framework.value} onSelect={(currentValue) => { setValue(currentValue === value ? "" : currentValue) setOpen(false) }} > {framework.label} <Check className={cn( "ml-auto", value === framework.value ? "opacity-100" : "opacity-0" )} /> </CommandItem> ))} </CommandGroup> </CommandList> </Command> </PopoverContent> </Popover> </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 }
Built using Popover and Command components for the perfect balance of functionality and accessibility.
npx shadcn@latest add combobox
Combobox patterns that work well
Technology and framework selection
The classic use case - let developers pick their tools:
import * as React from "react"import { Check, ChevronsUpDown } from "lucide-react"import { cn } from "@/lib/utils"import { Button } from "@/components/ui/button"import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList,} from "@/components/ui/command"import { Popover, PopoverContent, PopoverTrigger,} from "@/components/ui/popover"const frameworks = [ { value: "next.js", label: "Next.js", }, { value: "sveltekit", label: "SvelteKit", }, { value: "nuxt.js", label: "Nuxt.js", }, { value: "remix", label: "Remix", }, { value: "astro", label: "Astro", },]export default function ComboboxDemo() { const [open, setOpen] = React.useState(false) const [value, setValue] = React.useState("") return ( <div className="w-full p-6 flex justify-center"> <Popover open={open} onOpenChange={setOpen}> <PopoverTrigger asChild> <Button variant="outline" role="combobox" aria-expanded={open} className="w-[200px] justify-between" > {value ? frameworks.find((framework) => framework.value === value)?.label : "Select framework..."} <ChevronsUpDown className="opacity-50" /> </Button> </PopoverTrigger> <PopoverContent className="w-[200px] p-0"> <Command> <CommandInput placeholder="Search framework..." className="h-9" /> <CommandList> <CommandEmpty>No framework found.</CommandEmpty> <CommandGroup> {frameworks.map((framework) => ( <CommandItem key={framework.value} value={framework.value} onSelect={(currentValue) => { setValue(currentValue === value ? "" : currentValue) setOpen(false) }} > {framework.label} <Check className={cn( "ml-auto", value === framework.value ? "opacity-100" : "opacity-0" )} /> </CommandItem> ))} </CommandGroup> </CommandList> </Command> </PopoverContent> </Popover> </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 }
Status and workflow management
Perfect for project management and task tracking:
import * as React from "react"import { Button } from "@/components/ui/button"import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList,} from "@/components/ui/command"import { Popover, PopoverContent, PopoverTrigger,} from "@/components/ui/popover"type Status = { value: string label: string}const statuses: Status[] = [ { value: "backlog", label: "Backlog", }, { value: "todo", label: "Todo", }, { value: "in progress", label: "In Progress", }, { value: "done", label: "Done", }, { value: "canceled", label: "Canceled", },]export default function ComboboxPopover() { const [open, setOpen] = React.useState(false) const [selectedStatus, setSelectedStatus] = React.useState<Status | null>( null ) return ( <div className="w-full p-6 flex justify-center"> <div className="flex items-center space-x-4"> <p className="text-muted-foreground text-sm">Status</p> <Popover open={open} onOpenChange={setOpen}> <PopoverTrigger asChild> <Button variant="outline" className="w-[150px] justify-start"> {selectedStatus ? <>{selectedStatus.label}</> : <>+ Set status</>} </Button> </PopoverTrigger> <PopoverContent className="p-0" side="right" align="start"> <Command> <CommandInput placeholder="Change status..." /> <CommandList> <CommandEmpty>No results found.</CommandEmpty> <CommandGroup> {statuses.map((status) => ( <CommandItem key={status.value} value={status.value} onSelect={(value) => { setSelectedStatus( statuses.find((priority) => priority.value === value) || null ) setOpen(false) }} > {status.label} </CommandItem> ))} </CommandGroup> </CommandList> </Command> </PopoverContent> </Popover> </div> </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 }
Complex multi-action interfaces
When you need more than just selection:
import * as React from "react"import { MoreHorizontal } from "lucide-react"import { Button } from "@/components/ui/button"import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList,} from "@/components/ui/command"import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger,} from "@/components/ui/dropdown-menu"const labels = [ "feature", "bug", "enhancement", "documentation", "design", "question", "maintenance",]export default function ComboboxDropdownMenu() { const [label, setLabel] = React.useState("feature") const [open, setOpen] = React.useState(false) return ( <div className="w-full p-6 flex justify-center"> <div className="flex w-full flex-col items-start justify-between rounded-md border px-4 py-3 sm:flex-row sm:items-center"> <p className="text-sm leading-none font-medium"> <span className="bg-primary text-primary-foreground mr-2 rounded-lg px-2 py-1 text-xs"> {label} </span> <span className="text-muted-foreground">Create a new project</span> </p> <DropdownMenu open={open} onOpenChange={setOpen}> <DropdownMenuTrigger asChild> <Button variant="ghost" size="sm"> <MoreHorizontal /> </Button> </DropdownMenuTrigger> <DropdownMenuContent align="end" className="w-[200px]"> <DropdownMenuLabel>Actions</DropdownMenuLabel> <DropdownMenuGroup> <DropdownMenuItem>Assign to...</DropdownMenuItem> <DropdownMenuItem>Set due date...</DropdownMenuItem> <DropdownMenuSeparator /> <DropdownMenuSub> <DropdownMenuSubTrigger>Apply label</DropdownMenuSubTrigger> <DropdownMenuSubContent className="p-0"> <Command> <CommandInput placeholder="Filter label..." autoFocus={true} className="h-9" /> <CommandList> <CommandEmpty>No label found.</CommandEmpty> <CommandGroup> {labels.map((label) => ( <CommandItem key={label} value={label} onSelect={(value) => { setLabel(value) setOpen(false) }} > {label} </CommandItem> ))} </CommandGroup> </CommandList> </Command> </DropdownMenuSubContent> </DropdownMenuSub> <DropdownMenuSeparator /> <DropdownMenuItem className="text-red-600"> Delete <DropdownMenuShortcut>⌘⌫</DropdownMenuShortcut> </DropdownMenuItem> </DropdownMenuGroup> </DropdownMenuContent> </DropdownMenu> </div> </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 }
Mobile-friendly responsive design
Adapts to screen size for better mobile experience:
import * as React from "react"import { useMediaQuery } from "@/components/ui/shadcn-io/use-media-query"import { Button } from "@/components/ui/button"import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList,} from "@/components/ui/command"import { Drawer, DrawerContent, DrawerTrigger,} from "@/components/ui/drawer"import { Popover, PopoverContent, PopoverTrigger,} from "@/components/ui/popover"type Status = { value: string label: string}const statuses: Status[] = [ { value: "backlog", label: "Backlog", }, { value: "todo", label: "Todo", }, { value: "in progress", label: "In Progress", }, { value: "done", label: "Done", }, { value: "canceled", label: "Canceled", },]export default function ComboBoxResponsive() { const [open, setOpen] = React.useState(false) const isDesktop = useMediaQuery("(min-width: 768px)") const [selectedStatus, setSelectedStatus] = React.useState<Status | null>( null ) return ( <div className="w-full p-6 flex justify-center"> {isDesktop ? ( <Popover open={open} onOpenChange={setOpen}> <PopoverTrigger asChild> <Button variant="outline" className="w-[150px] justify-start"> {selectedStatus ? <>{selectedStatus.label}</> : <>+ Set status</>} </Button> </PopoverTrigger> <PopoverContent className="w-[200px] p-0" align="start"> <StatusList setOpen={setOpen} setSelectedStatus={setSelectedStatus} /> </PopoverContent> </Popover> ) : ( <Drawer open={open} onOpenChange={setOpen}> <DrawerTrigger asChild> <Button variant="outline" className="w-[150px] justify-start"> {selectedStatus ? <>{selectedStatus.label}</> : <>+ Set status</>} </Button> </DrawerTrigger> <DrawerContent> <div className="mt-4 border-t"> <StatusList setOpen={setOpen} setSelectedStatus={setSelectedStatus} /> </div> </DrawerContent> </Drawer> )} </div> )}function StatusList({ setOpen, setSelectedStatus,}: { setOpen: (open: boolean) => void setSelectedStatus: (status: Status | null) => void}) { return ( <Command> <CommandInput placeholder="Filter status..." /> <CommandList> <CommandEmpty>No results found.</CommandEmpty> <CommandGroup> {statuses.map((status) => ( <CommandItem key={status.value} value={status.value} onSelect={(value) => { setSelectedStatus( statuses.find((priority) => priority.value === value) || null ) setOpen(false) }} > {status.label} </CommandItem> ))} </CommandGroup> </CommandList> </Command> )}
"use client"import { useState } from "react"import { useIsomorphicLayoutEffect } from "@/components/ui/kibo-ui/use-isomorphic-layout-effect"type UseMediaQueryOptions = { defaultValue?: boolean initializeWithValue?: boolean}const IS_SERVER = typeof window === "undefined"export function useMediaQuery( query: string, { defaultValue = false, initializeWithValue = true, }: UseMediaQueryOptions = {},): boolean { const getMatches = (query: string): boolean => { if (IS_SERVER) { return defaultValue } return window.matchMedia(query).matches } const [matches, setMatches] = useState<boolean>(() => { if (initializeWithValue) { return getMatches(query) } return defaultValue }) // Handles the change event of the media query. function handleChange() { setMatches(getMatches(query)) } useIsomorphicLayoutEffect(() => { const matchMedia = window.matchMedia(query) // Triggered at the first client-side load and if query changes handleChange() // Use deprecated `addListener` and `removeListener` to support Safari < 14 (#135) if (matchMedia.addListener) { matchMedia.addListener(handleChange) } else { matchMedia.addEventListener("change", handleChange) } return () => { if (matchMedia.removeListener) { matchMedia.removeListener(handleChange) } else { matchMedia.removeEventListener("change", handleChange) } } }, [query]) return matches}export type { UseMediaQueryOptions }
Form integration with validation
Works seamlessly with react-hook-form and validation:
import { zodResolver } from "@hookform/resolvers/zod"import { Check, ChevronsUpDown } from "lucide-react"import { useForm } from "react-hook-form"import { toast } from "sonner"import { z } from "zod"import { cn } from "@/lib/utils"import { Button } from "@/components/ui/button"import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList,} from "@/components/ui/command"import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage,} from "@/components/ui/form"import { Popover, PopoverContent, PopoverTrigger,} from "@/components/ui/popover"const languages = [ { label: "English", value: "en" }, { label: "French", value: "fr" }, { label: "German", value: "de" }, { label: "Spanish", value: "es" }, { label: "Portuguese", value: "pt" }, { label: "Russian", value: "ru" }, { label: "Japanese", value: "ja" }, { label: "Korean", value: "ko" }, { label: "Chinese", value: "zh" },] as constconst FormSchema = z.object({ language: z.string({ required_error: "Please select a language.", }),})export default function ComboboxForm() { 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="w-full p-6 flex justify-center"> <Form {...form}> <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6"> <FormField control={form.control} name="language" render={({ field }) => ( <FormItem className="flex flex-col"> <FormLabel>Language</FormLabel> <Popover> <PopoverTrigger asChild> <FormControl> <Button variant="outline" role="combobox" className={cn( "w-[200px] justify-between", !field.value && "text-muted-foreground" )} > {field.value ? languages.find( (language) => language.value === field.value )?.label : "Select language"} <ChevronsUpDown className="opacity-50" /> </Button> </FormControl> </PopoverTrigger> <PopoverContent className="w-[200px] p-0"> <Command> <CommandInput placeholder="Search framework..." className="h-9" /> <CommandList> <CommandEmpty>No framework found.</CommandEmpty> <CommandGroup> {languages.map((language) => ( <CommandItem value={language.label} key={language.value} onSelect={() => { form.setValue("language", language.value) }} > {language.label} <Check className={cn( "ml-auto", language.value === field.value ? "opacity-100" : "opacity-0" )} /> </CommandItem> ))} </CommandGroup> </CommandList> </Command> </PopoverContent> </Popover> <FormDescription> This is the language that will be used in the dashboard. </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 }
What components work together?
The Combobox combines these components for the complete experience:
Component | What it does |
---|---|
Popover | Positions and shows/hides the dropdown |
PopoverTrigger | Button that opens the combobox when clicked |
PopoverContent | Container for the searchable options |
Command | Handles search, filtering, and keyboard navigation |
CommandInput | Search input for filtering options |
CommandList | Scrollable container for all options |
CommandItem | Individual selectable option |
CommandEmpty | Shown when no options match the search |
See Popover and Command for detailed APIs.
Combobox best practices
What makes comboboxes feel smooth and intuitive:
- Clear placeholder text - "Search countries..." tells users what they're picking from
- Show selection state - Display selected option in the trigger button
- Handle empty states - "No results found" when search returns nothing
- Debounce API calls - Wait 300ms before searching to avoid request spam
- Close on selection - Most users expect the dropdown to close after picking
- Keyboard shortcuts work - Arrow keys, Enter, Escape should all work properly
- Loading states for async - Show spinners when fetching search results
- Group related options - Categories like "Frontend" and "Backend" frameworks
Collapsible
React collapsible component with smooth animations and keyboard navigation. Perfect for FAQs, navigation menus, and content sections that need to expand and collapse.
Command
React command palette with fuzzy search, keyboard shortcuts, and dialog support. Perfect for app navigation, search interfaces, and power user workflows.