Combobox
Autocomplete input with search and selection for React and Next.js applications. Built with TypeScript support, Tailwind CSS styling, and shadcn/ui design featuring keyboard navigation and custom item creation.
'use client';import { Combobox, ComboboxContent, ComboboxEmpty, ComboboxGroup, ComboboxInput, ComboboxItem, ComboboxList, ComboboxTrigger,} from '@/components/ui/shadcn-io/combobox';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', }, { value: 'vite', label: 'Vite', },];const Example = () => ( <Combobox data={frameworks} onOpenChange={(open) => console.log('Combobox is open?', open)} onValueChange={(newValue) => console.log('Combobox value:', newValue)} type="framework" > <ComboboxTrigger /> <ComboboxContent> <ComboboxInput /> <ComboboxEmpty /> <ComboboxList> <ComboboxGroup> {frameworks.map((framework) => ( <ComboboxItem key={framework.value} value={framework.value}> {framework.label} </ComboboxItem> ))} </ComboboxGroup> </ComboboxList> </ComboboxContent> </Combobox>);export default Example;
'use client';import { useControllableState } from '@radix-ui/react-use-controllable-state';import { ChevronsUpDownIcon, PlusIcon } from 'lucide-react';import { type ComponentProps, createContext, type ReactNode, useContext, useEffect, useRef, useState,} from 'react';import { Button } from '@/components/ui/button';import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator,} from '@/components/ui/command';import { Popover, PopoverContent, PopoverTrigger,} from '@/components/ui/popover';import { cn } from '@/lib/utils';type ComboboxData = { label: string; value: string;};type ComboboxContextType = { data: ComboboxData[]; type: string; value: string; onValueChange: (value: string) => void; open: boolean; onOpenChange: (open: boolean) => void; width: number; setWidth: (width: number) => void; inputValue: string; setInputValue: (value: string) => void;};const ComboboxContext = createContext<ComboboxContextType>({ data: [], type: 'item', value: '', onValueChange: () => {}, open: false, onOpenChange: () => {}, width: 200, setWidth: () => {}, inputValue: '', setInputValue: () => {},});export type ComboboxProps = ComponentProps<typeof Popover> & { data: ComboboxData[]; type: string; defaultValue?: string; value?: string; onValueChange?: (value: string) => void; open?: boolean; onOpenChange?: (open: boolean) => void;};export const Combobox = ({ data, type, defaultValue, value: controlledValue, onValueChange: controlledOnValueChange, defaultOpen = false, open: controlledOpen, onOpenChange: controlledOnOpenChange, ...props}: ComboboxProps) => { const [value, onValueChange] = useControllableState({ defaultProp: defaultValue ?? '', prop: controlledValue, onChange: controlledOnValueChange, }); const [open, onOpenChange] = useControllableState({ defaultProp: defaultOpen, prop: controlledOpen, onChange: controlledOnOpenChange, }); const [width, setWidth] = useState(200); const [inputValue, setInputValue] = useState(''); return ( <ComboboxContext.Provider value={{ type, value, onValueChange, open, onOpenChange, data, width, setWidth, inputValue, setInputValue, }} > <Popover {...props} onOpenChange={onOpenChange} open={open} /> </ComboboxContext.Provider> );};export type ComboboxTriggerProps = ComponentProps<typeof Button>;export const ComboboxTrigger = ({ children, ...props}: ComboboxTriggerProps) => { const { value, data, type, setWidth } = useContext(ComboboxContext); const ref = useRef<HTMLButtonElement>(null); useEffect(() => { // Create a ResizeObserver to detect width changes const resizeObserver = new ResizeObserver((entries) => { for (const entry of entries) { const newWidth = (entry.target as HTMLElement).offsetWidth; if (newWidth) { setWidth?.(newWidth); } } }); if (ref.current) { resizeObserver.observe(ref.current); } // Clean up the observer when component unmounts return () => { resizeObserver.disconnect(); }; }, [setWidth]); return ( <PopoverTrigger asChild> <Button variant="outline" {...props} ref={ref}> {children ?? ( <span className="flex w-full items-center justify-between gap-2"> {value ? data.find((item) => item.value === value)?.label : `Select ${type}...`} <ChevronsUpDownIcon className="shrink-0 text-muted-foreground" size={16} /> </span> )} </Button> </PopoverTrigger> );};export type ComboboxContentProps = ComponentProps<typeof Command> & { popoverOptions?: ComponentProps<typeof PopoverContent>;};export const ComboboxContent = ({ className, popoverOptions, ...props}: ComboboxContentProps) => { const { width } = useContext(ComboboxContext); return ( <PopoverContent className={cn('p-0', className)} style={{ width }} {...popoverOptions} > <Command {...props} /> </PopoverContent> );};export type ComboboxInputProps = ComponentProps<typeof CommandInput> & { value?: string; defaultValue?: string; onValueChange?: (value: string) => void;};export const ComboboxInput = ({ value: controlledValue, defaultValue, onValueChange: controlledOnValueChange, ...props}: ComboboxInputProps) => { const { type, inputValue, setInputValue } = useContext(ComboboxContext); const [value, onValueChange] = useControllableState({ defaultProp: defaultValue ?? inputValue, prop: controlledValue, onChange: (newValue) => { // Sync with context state setInputValue(newValue); // Call external onChange if provided controlledOnValueChange?.(newValue); }, }); return ( <CommandInput onValueChange={onValueChange} placeholder={`Search ${type}...`} value={value} {...props} /> );};export type ComboboxListProps = ComponentProps<typeof CommandList>;export const ComboboxList = (props: ComboboxListProps) => ( <CommandList {...props} />);export type ComboboxEmptyProps = ComponentProps<typeof CommandEmpty>;export const ComboboxEmpty = ({ children, ...props }: ComboboxEmptyProps) => { const { type } = useContext(ComboboxContext); return ( <CommandEmpty {...props}>{children ?? `No ${type} found.`}</CommandEmpty> );};export type ComboboxGroupProps = ComponentProps<typeof CommandGroup>;export const ComboboxGroup = (props: ComboboxGroupProps) => ( <CommandGroup {...props} />);export type ComboboxItemProps = ComponentProps<typeof CommandItem>;export const ComboboxItem = (props: ComboboxItemProps) => { const { onValueChange, onOpenChange } = useContext(ComboboxContext); return ( <CommandItem onSelect={(currentValue) => { onValueChange(currentValue); onOpenChange(false); }} {...props} /> );};export type ComboboxSeparatorProps = ComponentProps<typeof CommandSeparator>;export const ComboboxSeparator = (props: ComboboxSeparatorProps) => ( <CommandSeparator {...props} />);export type ComboboxCreateNewProps = { onCreateNew: (value: string) => void; children?: (inputValue: string) => ReactNode; className?: string;};export const ComboboxCreateNew = ({ onCreateNew, children, className,}: ComboboxCreateNewProps) => { const { inputValue, type, onValueChange, onOpenChange } = useContext(ComboboxContext); if (!inputValue.trim()) { return null; } const handleCreateNew = () => { onCreateNew(inputValue.trim()); onValueChange(inputValue.trim()); onOpenChange(false); }; return ( <button className={cn( 'relative flex w-full cursor-pointer select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50', className )} onClick={handleCreateNew} type="button" > {children ? ( children(inputValue) ) : ( <> <PlusIcon className="h-4 w-4 text-muted-foreground" /> <span> Create new {type}: "{inputValue}" </span> </> )} </button> );};
Installation
npx shadcn@latest add https://www.shadcn.io/registry/combobox.json
npx shadcn@latest add https://www.shadcn.io/registry/combobox.json
pnpm dlx shadcn@latest add https://www.shadcn.io/registry/combobox.json
bunx shadcn@latest add https://www.shadcn.io/registry/combobox.json
Features
- Autocomplete search - Real-time filtering with dropdown suggestions using JavaScript
- Controlled/uncontrolled - State management flexibility for React form libraries
- Keyboard navigation - Arrow keys, enter, escape with full accessibility support
- Create new items - Dynamic item addition with custom validation using TypeScript
- Responsive design - Adaptive width and mobile-friendly interactions using Tailwind CSS
- Empty states - Customizable "no results" messaging with shadcn/ui styling
- Open source - Free autocomplete component for Next.js applications
Examples
Controlled version
'use client';import { Combobox, ComboboxContent, ComboboxEmpty, ComboboxGroup, ComboboxInput, ComboboxItem, ComboboxList, ComboboxTrigger,} from '@/components/ui/shadcn-io/combobox';import { useState } from 'react';const plants = [ { value: 'monstera-deliciosa', label: 'Monstera Deliciosa (Swiss Cheese Plant)', }, { value: 'ficus-lyrata', label: 'Ficus Lyrata (Fiddle Leaf Fig)', }, { value: 'sansevieria-trifasciata', label: 'Sansevieria Trifasciata (Snake Plant)', }, { value: 'spathiphyllum-wallisii', label: 'Spathiphyllum Wallisii (Peace Lily)', }, { value: 'epipremnum-aureum', label: 'Epipremnum Aureum (Golden Pothos)', }, { value: 'calathea-orbifolia', label: 'Calathea Orbifolia (Prayer Plant)', },];const Example = () => { const [open, setOpen] = useState(false); const [value, setValue] = useState(plants[0].value); return ( <Combobox data={plants} onOpenChange={setOpen} onValueChange={setValue} open={open} type="plant" value={value} > <ComboboxTrigger /> <ComboboxContent> <ComboboxInput /> <ComboboxEmpty /> <ComboboxList> <ComboboxGroup> {plants.map((plant) => ( <ComboboxItem key={plant.value} value={plant.value}> {plant.label} </ComboboxItem> ))} </ComboboxGroup> </ComboboxList> </ComboboxContent> </Combobox> );};export default Example;
'use client';import { useControllableState } from '@radix-ui/react-use-controllable-state';import { ChevronsUpDownIcon, PlusIcon } from 'lucide-react';import { type ComponentProps, createContext, type ReactNode, useContext, useEffect, useRef, useState,} from 'react';import { Button } from '@/components/ui/button';import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator,} from '@/components/ui/command';import { Popover, PopoverContent, PopoverTrigger,} from '@/components/ui/popover';import { cn } from '@/lib/utils';type ComboboxData = { label: string; value: string;};type ComboboxContextType = { data: ComboboxData[]; type: string; value: string; onValueChange: (value: string) => void; open: boolean; onOpenChange: (open: boolean) => void; width: number; setWidth: (width: number) => void; inputValue: string; setInputValue: (value: string) => void;};const ComboboxContext = createContext<ComboboxContextType>({ data: [], type: 'item', value: '', onValueChange: () => {}, open: false, onOpenChange: () => {}, width: 200, setWidth: () => {}, inputValue: '', setInputValue: () => {},});export type ComboboxProps = ComponentProps<typeof Popover> & { data: ComboboxData[]; type: string; defaultValue?: string; value?: string; onValueChange?: (value: string) => void; open?: boolean; onOpenChange?: (open: boolean) => void;};export const Combobox = ({ data, type, defaultValue, value: controlledValue, onValueChange: controlledOnValueChange, defaultOpen = false, open: controlledOpen, onOpenChange: controlledOnOpenChange, ...props}: ComboboxProps) => { const [value, onValueChange] = useControllableState({ defaultProp: defaultValue ?? '', prop: controlledValue, onChange: controlledOnValueChange, }); const [open, onOpenChange] = useControllableState({ defaultProp: defaultOpen, prop: controlledOpen, onChange: controlledOnOpenChange, }); const [width, setWidth] = useState(200); const [inputValue, setInputValue] = useState(''); return ( <ComboboxContext.Provider value={{ type, value, onValueChange, open, onOpenChange, data, width, setWidth, inputValue, setInputValue, }} > <Popover {...props} onOpenChange={onOpenChange} open={open} /> </ComboboxContext.Provider> );};export type ComboboxTriggerProps = ComponentProps<typeof Button>;export const ComboboxTrigger = ({ children, ...props}: ComboboxTriggerProps) => { const { value, data, type, setWidth } = useContext(ComboboxContext); const ref = useRef<HTMLButtonElement>(null); useEffect(() => { // Create a ResizeObserver to detect width changes const resizeObserver = new ResizeObserver((entries) => { for (const entry of entries) { const newWidth = (entry.target as HTMLElement).offsetWidth; if (newWidth) { setWidth?.(newWidth); } } }); if (ref.current) { resizeObserver.observe(ref.current); } // Clean up the observer when component unmounts return () => { resizeObserver.disconnect(); }; }, [setWidth]); return ( <PopoverTrigger asChild> <Button variant="outline" {...props} ref={ref}> {children ?? ( <span className="flex w-full items-center justify-between gap-2"> {value ? data.find((item) => item.value === value)?.label : `Select ${type}...`} <ChevronsUpDownIcon className="shrink-0 text-muted-foreground" size={16} /> </span> )} </Button> </PopoverTrigger> );};export type ComboboxContentProps = ComponentProps<typeof Command> & { popoverOptions?: ComponentProps<typeof PopoverContent>;};export const ComboboxContent = ({ className, popoverOptions, ...props}: ComboboxContentProps) => { const { width } = useContext(ComboboxContext); return ( <PopoverContent className={cn('p-0', className)} style={{ width }} {...popoverOptions} > <Command {...props} /> </PopoverContent> );};export type ComboboxInputProps = ComponentProps<typeof CommandInput> & { value?: string; defaultValue?: string; onValueChange?: (value: string) => void;};export const ComboboxInput = ({ value: controlledValue, defaultValue, onValueChange: controlledOnValueChange, ...props}: ComboboxInputProps) => { const { type, inputValue, setInputValue } = useContext(ComboboxContext); const [value, onValueChange] = useControllableState({ defaultProp: defaultValue ?? inputValue, prop: controlledValue, onChange: (newValue) => { // Sync with context state setInputValue(newValue); // Call external onChange if provided controlledOnValueChange?.(newValue); }, }); return ( <CommandInput onValueChange={onValueChange} placeholder={`Search ${type}...`} value={value} {...props} /> );};export type ComboboxListProps = ComponentProps<typeof CommandList>;export const ComboboxList = (props: ComboboxListProps) => ( <CommandList {...props} />);export type ComboboxEmptyProps = ComponentProps<typeof CommandEmpty>;export const ComboboxEmpty = ({ children, ...props }: ComboboxEmptyProps) => { const { type } = useContext(ComboboxContext); return ( <CommandEmpty {...props}>{children ?? `No ${type} found.`}</CommandEmpty> );};export type ComboboxGroupProps = ComponentProps<typeof CommandGroup>;export const ComboboxGroup = (props: ComboboxGroupProps) => ( <CommandGroup {...props} />);export type ComboboxItemProps = ComponentProps<typeof CommandItem>;export const ComboboxItem = (props: ComboboxItemProps) => { const { onValueChange, onOpenChange } = useContext(ComboboxContext); return ( <CommandItem onSelect={(currentValue) => { onValueChange(currentValue); onOpenChange(false); }} {...props} /> );};export type ComboboxSeparatorProps = ComponentProps<typeof CommandSeparator>;export const ComboboxSeparator = (props: ComboboxSeparatorProps) => ( <CommandSeparator {...props} />);export type ComboboxCreateNewProps = { onCreateNew: (value: string) => void; children?: (inputValue: string) => ReactNode; className?: string;};export const ComboboxCreateNew = ({ onCreateNew, children, className,}: ComboboxCreateNewProps) => { const { inputValue, type, onValueChange, onOpenChange } = useContext(ComboboxContext); if (!inputValue.trim()) { return null; } const handleCreateNew = () => { onCreateNew(inputValue.trim()); onValueChange(inputValue.trim()); onOpenChange(false); }; return ( <button className={cn( 'relative flex w-full cursor-pointer select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50', className )} onClick={handleCreateNew} type="button" > {children ? ( children(inputValue) ) : ( <> <PlusIcon className="h-4 w-4 text-muted-foreground" /> <span> Create new {type}: "{inputValue}" </span> </> )} </button> );};
Fixed width
'use client';import { Combobox, ComboboxContent, ComboboxEmpty, ComboboxGroup, ComboboxInput, ComboboxItem, ComboboxList, ComboboxTrigger,} from '@/components/ui/shadcn-io/combobox';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', }, { value: 'vite', label: 'Vite', },];const Example = () => ( <Combobox data={frameworks} onOpenChange={(open) => console.log('Combobox is open?', open)} onValueChange={(newValue) => console.log('Combobox value:', newValue)} type="framework" > <ComboboxTrigger className="w-[70%]" /> <ComboboxContent> <ComboboxInput /> <ComboboxEmpty /> <ComboboxList> <ComboboxGroup> {frameworks.map((framework) => ( <ComboboxItem key={framework.value} value={framework.value}> {framework.label} </ComboboxItem> ))} </ComboboxGroup> </ComboboxList> </ComboboxContent> </Combobox>);export default Example;
'use client';import { useControllableState } from '@radix-ui/react-use-controllable-state';import { ChevronsUpDownIcon, PlusIcon } from 'lucide-react';import { type ComponentProps, createContext, type ReactNode, useContext, useEffect, useRef, useState,} from 'react';import { Button } from '@/components/ui/button';import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator,} from '@/components/ui/command';import { Popover, PopoverContent, PopoverTrigger,} from '@/components/ui/popover';import { cn } from '@/lib/utils';type ComboboxData = { label: string; value: string;};type ComboboxContextType = { data: ComboboxData[]; type: string; value: string; onValueChange: (value: string) => void; open: boolean; onOpenChange: (open: boolean) => void; width: number; setWidth: (width: number) => void; inputValue: string; setInputValue: (value: string) => void;};const ComboboxContext = createContext<ComboboxContextType>({ data: [], type: 'item', value: '', onValueChange: () => {}, open: false, onOpenChange: () => {}, width: 200, setWidth: () => {}, inputValue: '', setInputValue: () => {},});export type ComboboxProps = ComponentProps<typeof Popover> & { data: ComboboxData[]; type: string; defaultValue?: string; value?: string; onValueChange?: (value: string) => void; open?: boolean; onOpenChange?: (open: boolean) => void;};export const Combobox = ({ data, type, defaultValue, value: controlledValue, onValueChange: controlledOnValueChange, defaultOpen = false, open: controlledOpen, onOpenChange: controlledOnOpenChange, ...props}: ComboboxProps) => { const [value, onValueChange] = useControllableState({ defaultProp: defaultValue ?? '', prop: controlledValue, onChange: controlledOnValueChange, }); const [open, onOpenChange] = useControllableState({ defaultProp: defaultOpen, prop: controlledOpen, onChange: controlledOnOpenChange, }); const [width, setWidth] = useState(200); const [inputValue, setInputValue] = useState(''); return ( <ComboboxContext.Provider value={{ type, value, onValueChange, open, onOpenChange, data, width, setWidth, inputValue, setInputValue, }} > <Popover {...props} onOpenChange={onOpenChange} open={open} /> </ComboboxContext.Provider> );};export type ComboboxTriggerProps = ComponentProps<typeof Button>;export const ComboboxTrigger = ({ children, ...props}: ComboboxTriggerProps) => { const { value, data, type, setWidth } = useContext(ComboboxContext); const ref = useRef<HTMLButtonElement>(null); useEffect(() => { // Create a ResizeObserver to detect width changes const resizeObserver = new ResizeObserver((entries) => { for (const entry of entries) { const newWidth = (entry.target as HTMLElement).offsetWidth; if (newWidth) { setWidth?.(newWidth); } } }); if (ref.current) { resizeObserver.observe(ref.current); } // Clean up the observer when component unmounts return () => { resizeObserver.disconnect(); }; }, [setWidth]); return ( <PopoverTrigger asChild> <Button variant="outline" {...props} ref={ref}> {children ?? ( <span className="flex w-full items-center justify-between gap-2"> {value ? data.find((item) => item.value === value)?.label : `Select ${type}...`} <ChevronsUpDownIcon className="shrink-0 text-muted-foreground" size={16} /> </span> )} </Button> </PopoverTrigger> );};export type ComboboxContentProps = ComponentProps<typeof Command> & { popoverOptions?: ComponentProps<typeof PopoverContent>;};export const ComboboxContent = ({ className, popoverOptions, ...props}: ComboboxContentProps) => { const { width } = useContext(ComboboxContext); return ( <PopoverContent className={cn('p-0', className)} style={{ width }} {...popoverOptions} > <Command {...props} /> </PopoverContent> );};export type ComboboxInputProps = ComponentProps<typeof CommandInput> & { value?: string; defaultValue?: string; onValueChange?: (value: string) => void;};export const ComboboxInput = ({ value: controlledValue, defaultValue, onValueChange: controlledOnValueChange, ...props}: ComboboxInputProps) => { const { type, inputValue, setInputValue } = useContext(ComboboxContext); const [value, onValueChange] = useControllableState({ defaultProp: defaultValue ?? inputValue, prop: controlledValue, onChange: (newValue) => { // Sync with context state setInputValue(newValue); // Call external onChange if provided controlledOnValueChange?.(newValue); }, }); return ( <CommandInput onValueChange={onValueChange} placeholder={`Search ${type}...`} value={value} {...props} /> );};export type ComboboxListProps = ComponentProps<typeof CommandList>;export const ComboboxList = (props: ComboboxListProps) => ( <CommandList {...props} />);export type ComboboxEmptyProps = ComponentProps<typeof CommandEmpty>;export const ComboboxEmpty = ({ children, ...props }: ComboboxEmptyProps) => { const { type } = useContext(ComboboxContext); return ( <CommandEmpty {...props}>{children ?? `No ${type} found.`}</CommandEmpty> );};export type ComboboxGroupProps = ComponentProps<typeof CommandGroup>;export const ComboboxGroup = (props: ComboboxGroupProps) => ( <CommandGroup {...props} />);export type ComboboxItemProps = ComponentProps<typeof CommandItem>;export const ComboboxItem = (props: ComboboxItemProps) => { const { onValueChange, onOpenChange } = useContext(ComboboxContext); return ( <CommandItem onSelect={(currentValue) => { onValueChange(currentValue); onOpenChange(false); }} {...props} /> );};export type ComboboxSeparatorProps = ComponentProps<typeof CommandSeparator>;export const ComboboxSeparator = (props: ComboboxSeparatorProps) => ( <CommandSeparator {...props} />);export type ComboboxCreateNewProps = { onCreateNew: (value: string) => void; children?: (inputValue: string) => ReactNode; className?: string;};export const ComboboxCreateNew = ({ onCreateNew, children, className,}: ComboboxCreateNewProps) => { const { inputValue, type, onValueChange, onOpenChange } = useContext(ComboboxContext); if (!inputValue.trim()) { return null; } const handleCreateNew = () => { onCreateNew(inputValue.trim()); onValueChange(inputValue.trim()); onOpenChange(false); }; return ( <button className={cn( 'relative flex w-full cursor-pointer select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50', className )} onClick={handleCreateNew} type="button" > {children ? ( children(inputValue) ) : ( <> <PlusIcon className="h-4 w-4 text-muted-foreground" /> <span> Create new {type}: "{inputValue}" </span> </> )} </button> );};
Create new items
'use client';import { Combobox, ComboboxContent, ComboboxCreateNew, ComboboxEmpty, ComboboxGroup, ComboboxInput, ComboboxItem, ComboboxList, ComboboxSeparator, ComboboxTrigger,} from '@/components/ui/shadcn-io/combobox';import { useState } from 'react';const initialFrameworks = [ { value: 'next.js', label: 'Next.js', }, { value: 'sveltekit', label: 'SvelteKit', }, { value: 'nuxt.js', label: 'Nuxt.js', }, { value: 'remix', label: 'Remix', }, { value: 'astro', label: 'Astro', }, { value: 'vite', label: 'Vite', },];const Example = () => { const [frameworks, setFrameworks] = useState(initialFrameworks); const [value, setValue] = useState(''); const handleCreateNew = (newValue: string) => { console.log('Creating new framework:', newValue); // Add the new framework to the list const newFramework = { value: newValue.toLowerCase().replace(/\s+/g, '-'), label: newValue, }; setFrameworks((prev) => [...prev, newFramework]); setValue(newFramework.value); }; return ( <Combobox data={frameworks} onValueChange={setValue} type="framework" value={value} > <ComboboxTrigger className="w-[300px]" /> <ComboboxContent> <ComboboxInput /> <ComboboxEmpty> <ComboboxCreateNew onCreateNew={handleCreateNew} /> </ComboboxEmpty> <ComboboxList> <ComboboxGroup> {frameworks.map((framework) => ( <ComboboxItem key={framework.value} value={framework.value}> {framework.label} </ComboboxItem> ))} </ComboboxGroup> </ComboboxList> </ComboboxContent> </Combobox> );};export default Example;
'use client';import { useControllableState } from '@radix-ui/react-use-controllable-state';import { ChevronsUpDownIcon, PlusIcon } from 'lucide-react';import { type ComponentProps, createContext, type ReactNode, useContext, useEffect, useRef, useState,} from 'react';import { Button } from '@/components/ui/button';import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator,} from '@/components/ui/command';import { Popover, PopoverContent, PopoverTrigger,} from '@/components/ui/popover';import { cn } from '@/lib/utils';type ComboboxData = { label: string; value: string;};type ComboboxContextType = { data: ComboboxData[]; type: string; value: string; onValueChange: (value: string) => void; open: boolean; onOpenChange: (open: boolean) => void; width: number; setWidth: (width: number) => void; inputValue: string; setInputValue: (value: string) => void;};const ComboboxContext = createContext<ComboboxContextType>({ data: [], type: 'item', value: '', onValueChange: () => {}, open: false, onOpenChange: () => {}, width: 200, setWidth: () => {}, inputValue: '', setInputValue: () => {},});export type ComboboxProps = ComponentProps<typeof Popover> & { data: ComboboxData[]; type: string; defaultValue?: string; value?: string; onValueChange?: (value: string) => void; open?: boolean; onOpenChange?: (open: boolean) => void;};export const Combobox = ({ data, type, defaultValue, value: controlledValue, onValueChange: controlledOnValueChange, defaultOpen = false, open: controlledOpen, onOpenChange: controlledOnOpenChange, ...props}: ComboboxProps) => { const [value, onValueChange] = useControllableState({ defaultProp: defaultValue ?? '', prop: controlledValue, onChange: controlledOnValueChange, }); const [open, onOpenChange] = useControllableState({ defaultProp: defaultOpen, prop: controlledOpen, onChange: controlledOnOpenChange, }); const [width, setWidth] = useState(200); const [inputValue, setInputValue] = useState(''); return ( <ComboboxContext.Provider value={{ type, value, onValueChange, open, onOpenChange, data, width, setWidth, inputValue, setInputValue, }} > <Popover {...props} onOpenChange={onOpenChange} open={open} /> </ComboboxContext.Provider> );};export type ComboboxTriggerProps = ComponentProps<typeof Button>;export const ComboboxTrigger = ({ children, ...props}: ComboboxTriggerProps) => { const { value, data, type, setWidth } = useContext(ComboboxContext); const ref = useRef<HTMLButtonElement>(null); useEffect(() => { // Create a ResizeObserver to detect width changes const resizeObserver = new ResizeObserver((entries) => { for (const entry of entries) { const newWidth = (entry.target as HTMLElement).offsetWidth; if (newWidth) { setWidth?.(newWidth); } } }); if (ref.current) { resizeObserver.observe(ref.current); } // Clean up the observer when component unmounts return () => { resizeObserver.disconnect(); }; }, [setWidth]); return ( <PopoverTrigger asChild> <Button variant="outline" {...props} ref={ref}> {children ?? ( <span className="flex w-full items-center justify-between gap-2"> {value ? data.find((item) => item.value === value)?.label : `Select ${type}...`} <ChevronsUpDownIcon className="shrink-0 text-muted-foreground" size={16} /> </span> )} </Button> </PopoverTrigger> );};export type ComboboxContentProps = ComponentProps<typeof Command> & { popoverOptions?: ComponentProps<typeof PopoverContent>;};export const ComboboxContent = ({ className, popoverOptions, ...props}: ComboboxContentProps) => { const { width } = useContext(ComboboxContext); return ( <PopoverContent className={cn('p-0', className)} style={{ width }} {...popoverOptions} > <Command {...props} /> </PopoverContent> );};export type ComboboxInputProps = ComponentProps<typeof CommandInput> & { value?: string; defaultValue?: string; onValueChange?: (value: string) => void;};export const ComboboxInput = ({ value: controlledValue, defaultValue, onValueChange: controlledOnValueChange, ...props}: ComboboxInputProps) => { const { type, inputValue, setInputValue } = useContext(ComboboxContext); const [value, onValueChange] = useControllableState({ defaultProp: defaultValue ?? inputValue, prop: controlledValue, onChange: (newValue) => { // Sync with context state setInputValue(newValue); // Call external onChange if provided controlledOnValueChange?.(newValue); }, }); return ( <CommandInput onValueChange={onValueChange} placeholder={`Search ${type}...`} value={value} {...props} /> );};export type ComboboxListProps = ComponentProps<typeof CommandList>;export const ComboboxList = (props: ComboboxListProps) => ( <CommandList {...props} />);export type ComboboxEmptyProps = ComponentProps<typeof CommandEmpty>;export const ComboboxEmpty = ({ children, ...props }: ComboboxEmptyProps) => { const { type } = useContext(ComboboxContext); return ( <CommandEmpty {...props}>{children ?? `No ${type} found.`}</CommandEmpty> );};export type ComboboxGroupProps = ComponentProps<typeof CommandGroup>;export const ComboboxGroup = (props: ComboboxGroupProps) => ( <CommandGroup {...props} />);export type ComboboxItemProps = ComponentProps<typeof CommandItem>;export const ComboboxItem = (props: ComboboxItemProps) => { const { onValueChange, onOpenChange } = useContext(ComboboxContext); return ( <CommandItem onSelect={(currentValue) => { onValueChange(currentValue); onOpenChange(false); }} {...props} /> );};export type ComboboxSeparatorProps = ComponentProps<typeof CommandSeparator>;export const ComboboxSeparator = (props: ComboboxSeparatorProps) => ( <CommandSeparator {...props} />);export type ComboboxCreateNewProps = { onCreateNew: (value: string) => void; children?: (inputValue: string) => ReactNode; className?: string;};export const ComboboxCreateNew = ({ onCreateNew, children, className,}: ComboboxCreateNewProps) => { const { inputValue, type, onValueChange, onOpenChange } = useContext(ComboboxContext); if (!inputValue.trim()) { return null; } const handleCreateNew = () => { onCreateNew(inputValue.trim()); onValueChange(inputValue.trim()); onOpenChange(false); }; return ( <button className={cn( 'relative flex w-full cursor-pointer select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50', className )} onClick={handleCreateNew} type="button" > {children ? ( children(inputValue) ) : ( <> <PlusIcon className="h-4 w-4 text-muted-foreground" /> <span> Create new {type}: "{inputValue}" </span> </> )} </button> );};
Use Cases
- Search interfaces - User/product/content lookup with autocomplete
- Tag inputs - Multi-select with creation of new tags
- Command palettes - Quick actions and navigation menus
- Form selects - Enhanced dropdowns with search and filtering
Implementation
Works with React Hook Form and other form libraries. Supports async data loading. Use createNew
prop for dynamic item creation. Built on Radix UI Command primitives.
Color Picker
Interactive color selection component for React and Next.js applications. Built with TypeScript support, Tailwind CSS styling, and shadcn/ui design featuring drag controls, eyedropper tool, and multiple format outputs.
Dropzone
Drag-and-drop file upload component for React and Next.js applications. Built with TypeScript support, Tailwind CSS styling, and shadcn/ui design featuring file validation, preview, and customizable upload interfaces.