Combobox
A combobox component that allows users to search and select from a list of options.
Preview
Loading...
import { useState, useId } from "react";
import {
Combobox,
ComboboxInput,
ComboboxTrigger,
ComboboxClear,
ComboboxValue,
ComboboxPortal,
ComboboxPositioner,
ComboboxPopup,
ComboboxList,
ComboboxItem,
ComboboxItemIndicator,
ComboboxEmpty,
ComboboxChips,
ComboboxChip,
ComboboxChipRemove,
ComboboxGroup,
ComboboxGroupLabel,
ComboboxSeparator,
} from "@/components/ui/Combobox";
import { Button } from "@/components/ui/Button";
import { Label } from "@/components/ui/Label";
import { CheckIcon, Cross2Icon, ChevronDownIcon } from "@radix-ui/react-icons";
export function ComboboxDemo() {
return {
const id = useId();
return (
<div className="space-y-1">
<Label htmlFor={id}>Choose a fruit</Label>
<Combobox items={fruits}>
<div className="flex items-center gap-0.5 border border-gray-6 rounded-sm px-2 py-1.5 focus-within:ring-2 focus-within:ring-gray-8">
<ComboboxInput
id={id}
placeholder="e.g. Apple"
className="flex-1 outline-none bg-transparent"
/>
<div className="flex items-center gap-0.5">
<ComboboxClear
className="p-0.5 hover:bg-accent-3 rounded-sm cursor-pointer"
aria-label="Clear selection"
>
<Cross2Icon className="size-1" />
</ComboboxClear>
<ComboboxTrigger
className="p-0.5 hover:bg-accent-3 rounded-sm cursor-pointer"
aria-label="Open popup"
>
<ChevronDownIcon className="size-1" />
</ComboboxTrigger>
</div>
</div>
<ComboboxPortal>
<ComboboxPositioner sideOffset={4}>
<ComboboxPopup className="w-40">
<ComboboxEmpty>No fruits found.</ComboboxEmpty>
<ComboboxList>
{(item: string) => (
<ComboboxItem key={item} value={item}>
<ComboboxItemIndicator>
<CheckIcon className="size-1" />
</ComboboxItemIndicator>
<span>{item}</span>
</ComboboxItem>
)}
</ComboboxList>
</ComboboxPopup>
</ComboboxPositioner>
</ComboboxPortal>
</Combobox>
</div>
);
};Installation
Make sure that namespace is set in your component.json file. Namespace docs: Learn more about namespaces
pnpm dlx shadcn@latest add @aura/comboboxManual
Install the following dependencies:
pnpm install @base-ui/reactCopy and paste the class names utility into your utils/class-names.ts file.
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
Copy and paste the Combobox component into your components/ui/Combobox.tsx file.
"use client";
/**
* @description A combobox component that allows users to search and select from a list of options.
*/
import * as React from "react";
import { Combobox } from "@base-ui/react/combobox";
import { cn } from "@/utils/class-names";
const ComboboxRoot = Combobox.Root;
function ComboboxInput({
className,
...props
}: React.ComponentProps<typeof Combobox.Input>) {
return (
<Combobox.Input
data-slot="combobox-input"
className={cn(className)}
{...props}
/>
);
}
function ComboboxTrigger({
className,
...props
}: React.ComponentProps<typeof Combobox.Trigger>) {
return (
<Combobox.Trigger
data-slot="combobox-trigger"
className={cn(className)}
{...props}
/>
);
}
function ComboboxIcon({
className,
...props
}: React.ComponentProps<typeof Combobox.Icon>) {
return (
<Combobox.Icon
data-slot="combobox-icon"
className={cn(className)}
{...props}
/>
);
}
function ComboboxClear({
className,
...props
}: React.ComponentProps<typeof Combobox.Clear>) {
return (
<Combobox.Clear
data-slot="combobox-clear"
className={cn(className)}
{...props}
/>
);
}
function ComboboxValue({
...props
}: React.ComponentProps<typeof Combobox.Value>) {
return <Combobox.Value data-slot="combobox-value" {...props} />;
}
function ComboboxChips({
className,
...props
}: React.ComponentProps<typeof Combobox.Chips>) {
return (
<Combobox.Chips
data-slot="combobox-chips"
className={cn(className)}
{...props}
/>
);
}
function ComboboxChip({
className,
...props
}: React.ComponentProps<typeof Combobox.Chip>) {
return (
<Combobox.Chip
data-slot="combobox-chip"
className={cn(className)}
{...props}
/>
);
}
function ComboboxChipRemove({
className,
...props
}: React.ComponentProps<typeof Combobox.ChipRemove>) {
return (
<Combobox.ChipRemove
data-slot="combobox-chip-remove"
className={cn(className)}
{...props}
/>
);
}
function ComboboxPortal({
...props
}: React.ComponentProps<typeof Combobox.Portal>) {
return <Combobox.Portal data-slot="combobox-portal" {...props} />;
}
function ComboboxBackdrop({
className,
...props
}: React.ComponentProps<typeof Combobox.Backdrop>) {
return (
<Combobox.Backdrop
data-slot="combobox-backdrop"
className={cn(className)}
{...props}
/>
);
}
function ComboboxPositioner({
className,
...props
}: React.ComponentProps<typeof Combobox.Positioner>) {
return (
<Combobox.Positioner
data-slot="combobox-positioner"
collisionPadding={8}
className={cn("outline-0", className)}
{...props}
/>
);
}
function ComboboxPopup({
className,
...props
}: React.ComponentProps<typeof Combobox.Popup>) {
return (
<Combobox.Popup
data-slot="combobox-popup"
className={cn(
"bg-gray-1 border border-gray-a6 rounded-sm relative shadow-md w-[var(--anchor-width)] max-h-[23rem] max-w-[var(--available-width)]",
className
)}
{...props}
/>
);
}
function ComboboxArrow({
className,
...props
}: React.ComponentProps<typeof Combobox.Arrow>) {
return (
<Combobox.Arrow
data-slot="combobox-arrow"
className={cn(className, "fill-gray-1")}
{...props}
/>
);
}
function ComboboxStatus({
className,
...props
}: React.ComponentProps<typeof Combobox.Status>) {
return (
<Combobox.Status
data-slot="combobox-status"
className={cn(className)}
{...props}
/>
);
}
function ComboboxEmpty({
className,
...props
}: React.ComponentProps<typeof Combobox.Empty>) {
return (
<Combobox.Empty
data-slot="combobox-empty"
className={cn(
"text-gray-11 py-1 px-2 text-sm text-center empty:hidden",
className
)}
{...props}
/>
);
}
function ComboboxList({
className,
...props
}: React.ComponentProps<typeof Combobox.List>) {
return (
<Combobox.List
data-slot="combobox-list"
className={cn(
"overflow-y-auto overscroll-contain py-0.5 scroll-py-0.5 outline-0 max-h-[min(23rem,var(--available-height))] data-[empty]:p-0",
className
)}
{...props}
/>
);
}
function ComboboxRow({
className,
...props
}: React.ComponentProps<typeof Combobox.Row>) {
return (
<Combobox.Row
data-slot="combobox-row"
className={cn(className)}
{...props}
/>
);
}
function ComboboxItem({
className,
...props
}: React.ComponentProps<typeof Combobox.Item>) {
return (
<Combobox.Item
data-slot="combobox-item"
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm p-0.5 px-2 text-sm outline-none hover:bg-accent-3 focus:bg-accent-3 data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
/>
);
}
function ComboboxItemIndicator({
className,
...props
}: React.ComponentProps<typeof Combobox.ItemIndicator>) {
return (
<Combobox.ItemIndicator
data-slot="combobox-item-indicator"
className={cn(
"absolute right-0.5 top-0 bottom-0 flex items-center justify-center",
className
)}
{...props}
/>
);
}
function ComboboxSeparator({
className,
...props
}: React.ComponentProps<typeof Combobox.Separator>) {
return (
<Combobox.Separator
data-slot="combobox-separator"
className={cn("m-0.6 h-px bg-gray-a6", className)}
{...props}
/>
);
}
function ComboboxGroup({
className,
...props
}: React.ComponentProps<typeof Combobox.Group>) {
return (
<Combobox.Group
data-slot="combobox-group"
className={cn(className)}
{...props}
/>
);
}
function ComboboxGroupLabel({
className,
...props
}: React.ComponentProps<typeof Combobox.GroupLabel>) {
return (
<Combobox.GroupLabel
data-slot="combobox-group-label"
className={cn(
"p-0.5 px-2 text-xs font-medium text-gray-12",
className
)}
{...props}
/>
);
}
function ComboboxCollection({
...props
}: React.ComponentProps<typeof Combobox.Collection>) {
return (
<Combobox.Collection
data-slot="combobox-collection"
{...props}
/>
);
}
export {
ComboboxRoot as Combobox,
ComboboxInput,
ComboboxTrigger,
ComboboxIcon,
ComboboxClear,
ComboboxValue,
ComboboxChips,
ComboboxChip,
ComboboxChipRemove,
ComboboxPortal,
ComboboxBackdrop,
ComboboxPositioner,
ComboboxPopup,
ComboboxArrow,
ComboboxStatus,
ComboboxEmpty,
ComboboxList,
ComboboxRow,
ComboboxItem,
ComboboxItemIndicator,
ComboboxSeparator,
ComboboxGroup,
ComboboxGroupLabel,
ComboboxCollection,
};
Usage
WithClearButton
export const WithClearButton = () => {
const id = `combobox-clear-${Math.random().toString(36).substr(2, 9)}`;
return (
<div className="space-y-1">
<Label htmlFor={id}>Select a fruit</Label>
<Combobox items={fruits}>
<div className="flex items-center gap-0.5 border border-gray-6 rounded-sm px-2 py-1.5 focus-within:ring-2 focus-within:ring-gray-8">
<ComboboxInput
id={id}
placeholder="Search fruits..."
className="flex-1 outline-none bg-transparent"
/>
<ComboboxClear
className="p-0.5 hover:bg-accent-3 rounded-sm cursor-pointer"
aria-label="Clear selection"
>
<Cross2Icon className="size-1" />
</ComboboxClear>
</div>
<ComboboxPortal>
<ComboboxPositioner sideOffset={4}>
<ComboboxPopup className="w-40">
<ComboboxEmpty>No fruits found.</ComboboxEmpty>
<ComboboxList>
{(item: string) => (
<ComboboxItem key={item} value={item}>
<ComboboxItemIndicator>
<CheckIcon className="size-1" />
</ComboboxItemIndicator>
<span>{item}</span>
</ComboboxItem>
)}
</ComboboxList>
</ComboboxPopup>
</ComboboxPositioner>
</ComboboxPortal>
</Combobox>
</div>
);
};WithTriggerButton
export const WithTriggerButton = () => {
const id = useId();
return (
<div className="space-y-1">
<Label htmlFor={id}>Choose a fruit</Label>
<Combobox items={fruits}>
<div className="flex items-center gap-0.5 border border-gray-6 rounded-sm px-2 py-1.5 focus-within:ring-2 focus-within:ring-gray-8">
<ComboboxInput
id={id}
placeholder="Select a fruit"
className="flex-1 outline-none bg-transparent"
/>
<ComboboxTrigger
className="p-0.5 hover:bg-accent-3 rounded-sm cursor-pointer"
aria-label="Open popup"
>
<ChevronDownIcon className="size-1" />
</ComboboxTrigger>
</div>
<ComboboxPortal>
<ComboboxPositioner sideOffset={4}>
<ComboboxPopup className="w-40">
<ComboboxEmpty>No fruits found.</ComboboxEmpty>
<ComboboxList>
{(item: string) => (
<ComboboxItem key={item} value={item}>
<ComboboxItemIndicator>
<CheckIcon className="size-1" />
</ComboboxItemIndicator>
<span>{item}</span>
</ComboboxItem>
)}
</ComboboxList>
</ComboboxPopup>
</ComboboxPositioner>
</ComboboxPortal>
</Combobox>
</div>
);
};WithChips
export const WithChips = () => {
const id = useId();
return (
<div className="space-y-1">
<Label htmlFor={id}>Select multiple fruits</Label>
<Combobox items={fruits} multiple>
<ComboboxChips className="min-h-9 flex-wrap gap-1.5 border border-gray-6 rounded-sm px-2 py-1.5 focus-within:ring-2 focus-within:ring-gray-8">
<ComboboxChip>
<ComboboxValue />
<ComboboxChipRemove>
<Cross2Icon className="size-1" />
</ComboboxChipRemove>
</ComboboxChip>
<ComboboxInput
id={id}
placeholder="Add fruits..."
className="min-w-16 flex-1 outline-none bg-transparent"
/>
</ComboboxChips>
<ComboboxPortal>
<ComboboxPositioner sideOffset={4}>
<ComboboxPopup className="w-40">
<ComboboxEmpty>No fruits found.</ComboboxEmpty>
<ComboboxList>
{(item: string) => (
<ComboboxItem key={item} value={item}>
<ComboboxItemIndicator>
<CheckIcon className="size-1" />
</ComboboxItemIndicator>
<span>{item}</span>
</ComboboxItem>
)}
</ComboboxList>
</ComboboxPopup>
</ComboboxPositioner>
</ComboboxPortal>
</Combobox>
</div>
);
};WithGroups
export const WithGroups = () => {
const id = useId();
const regions = Array.from(new Set(countries.map((c) => c.region)));
return (
<div className="space-y-1">
<Label htmlFor={id}>Select a country</Label>
<Combobox items={countries} getItemValue={(item) => item.value}>
<div className="flex items-center gap-0.5 border border-gray-6 rounded-sm px-2 py-1.5 focus-within:ring-2 focus-within:ring-gray-8">
<ComboboxInput
id={id}
placeholder="Search countries..."
className="flex-1 outline-none bg-transparent"
/>
<div className="flex items-center gap-0.5">
<ComboboxClear
className="p-0.5 hover:bg-accent-3 rounded-sm cursor-pointer"
aria-label="Clear selection"
>
<Cross2Icon className="size-1" />
</ComboboxClear>
<ComboboxTrigger
className="p-0.5 hover:bg-accent-3 rounded-sm cursor-pointer"
aria-label="Open popup"
>
<ChevronDownIcon className="size-1" />
</ComboboxTrigger>
</div>
</div>
<ComboboxPortal>
<ComboboxPositioner sideOffset={4}>
<ComboboxPopup className="w-40">
<ComboboxEmpty>No countries found.</ComboboxEmpty>
<ComboboxList>
{regions.map((region) => (
<ComboboxGroup key={region}>
<ComboboxGroupLabel>{region}</ComboboxGroupLabel>
{countries
.filter((c) => c.region === region)
.map((country) => (
<ComboboxItem key={country.value} value={country.value}>
<ComboboxItemIndicator>
<CheckIcon className="size-1" />
</ComboboxItemIndicator>
<span>{country.label}</span>
</ComboboxItem>
))}
</ComboboxGroup>
))}
</ComboboxList>
</ComboboxPopup>
</ComboboxPositioner>
</ComboboxPortal>
</Combobox>
</div>
);
};WithSeparator
export const WithSeparator = () => {
const id = useId();
const popularFruits = ["Apple", "Banana", "Orange", "Grape"];
const otherFruits = fruits.filter((f) => !popularFruits.includes(f));
return (
<div className="space-y-1">
<Label htmlFor={id}>Choose a fruit</Label>
<Combobox items={fruits}>
<div className="flex items-center gap-0.5 border border-gray-6 rounded-sm px-2 py-1.5 focus-within:ring-2 focus-within:ring-gray-8">
<ComboboxInput
id={id}
placeholder="e.g. Apple"
className="flex-1 outline-none bg-transparent"
/>
<div className="flex items-center gap-0.5">
<ComboboxClear
className="p-0.5 hover:bg-accent-3 rounded-sm cursor-pointer"
aria-label="Clear selection"
>
<Cross2Icon className="size-1" />
</ComboboxClear>
<ComboboxTrigger
className="p-0.5 hover:bg-accent-3 rounded-sm cursor-pointer"
aria-label="Open popup"
>
<ChevronDownIcon className="size-1" />
</ComboboxTrigger>
</div>
</div>
<ComboboxPortal>
<ComboboxPositioner sideOffset={4}>
<ComboboxPopup className="w-40">
<ComboboxEmpty>No fruits found.</ComboboxEmpty>
<ComboboxList>
<ComboboxGroup>
<ComboboxGroupLabel>Popular</ComboboxGroupLabel>
{popularFruits.map((item) => (
<ComboboxItem key={item} value={item}>
<ComboboxItemIndicator>
<CheckIcon className="size-1" />
</ComboboxItemIndicator>
<span>{item}</span>
</ComboboxItem>
))}
</ComboboxGroup>
<ComboboxSeparator />
<ComboboxGroup>
<ComboboxGroupLabel>Others</ComboboxGroupLabel>
{otherFruits.map((item) => (
<ComboboxItem key={item} value={item}>
<ComboboxItemIndicator>
<CheckIcon className="size-1" />
</ComboboxItemIndicator>
<span>{item}</span>
</ComboboxItem>
))}
</ComboboxGroup>
</ComboboxList>
</ComboboxPopup>
</ComboboxPositioner>
</ComboboxPortal>
</Combobox>
</div>
);
};Controlled
export const Controlled = () => {
const [value, setValue] = useState<string | null>(null);
const id = useId();
return (
<div className="space-y-1">
<div className="flex items-center gap-0.5">
<span className="text-sm text-gray-11">Selected:</span>
<span className="text-sm font-medium">{value || "None"}</span>
</div>
<Label htmlFor={id}>Choose a fruit</Label>
<Combobox items={fruits} value={value} onValueChange={setValue}>
<div className="flex items-center gap-0.5 border border-gray-6 rounded-sm px-2 py-1.5 focus-within:ring-2 focus-within:ring-gray-8">
<ComboboxInput
id={id}
placeholder="e.g. Apple"
className="flex-1 outline-none bg-transparent"
/>
<div className="flex items-center gap-0.5">
<ComboboxClear
className="p-0.5 hover:bg-accent-3 rounded-sm cursor-pointer"
aria-label="Clear selection"
>
<Cross2Icon className="size-1" />
</ComboboxClear>
<ComboboxTrigger
className="p-0.5 hover:bg-accent-3 rounded-sm cursor-pointer"
aria-label="Open popup"
>
<ChevronDownIcon className="size-1" />
</ComboboxTrigger>
</div>
</div>
<ComboboxPortal>
<ComboboxPositioner sideOffset={4}>
<ComboboxPopup className="w-40">
<ComboboxEmpty>No fruits found.</ComboboxEmpty>
<ComboboxList>
{(item: string) => (
<ComboboxItem key={item} value={item}>
<ComboboxItemIndicator>
<CheckIcon className="size-1" />
</ComboboxItemIndicator>
<span>{item}</span>
</ComboboxItem>
)}
</ComboboxList>
</ComboboxPopup>
</ComboboxPositioner>
</ComboboxPortal>
</Combobox>
<Button
variant="link"
size="sm"
onClick={() => setValue(null)}
className="mt-0.5"
>
Clear Selection
</Button>
</div>
);
};Disabled
export const Disabled = () => {
const id = useId();
return (
<div className="space-y-1">
<Label htmlFor={id}>Choose a fruit (disabled)</Label>
<Combobox items={fruits} disabled>
<div className="flex items-center gap-0.5 border border-gray-6 rounded-sm px-2 py-1.5 opacity-50 cursor-not-allowed">
<ComboboxInput
id={id}
placeholder="e.g. Apple"
className="flex-1 outline-none bg-transparent"
disabled
/>
<div className="flex items-center gap-0.5">
<ComboboxClear
className="p-0.5 hover:bg-accent-3 rounded-sm cursor-pointer"
aria-label="Clear selection"
disabled
>
<Cross2Icon className="size-1" />
</ComboboxClear>
<ComboboxTrigger
className="p-0.5 hover:bg-accent-3 rounded-sm cursor-pointer"
aria-label="Open popup"
disabled
>
<ChevronDownIcon className="size-1" />
</ComboboxTrigger>
</div>
</div>
<ComboboxPortal>
<ComboboxPositioner sideOffset={4}>
<ComboboxPopup className="w-40">
<ComboboxEmpty>No fruits found.</ComboboxEmpty>
<ComboboxList>
{(item: string) => (
<ComboboxItem key={item} value={item}>
<ComboboxItemIndicator>
<CheckIcon className="size-1" />
</ComboboxItemIndicator>
<span>{item}</span>
</ComboboxItem>
)}
</ComboboxList>
</ComboboxPopup>
</ComboboxPositioner>
</ComboboxPortal>
</Combobox>
</div>
);
};Positioning
export const Positioning = () => {
const id = useId();
return (
<div className="grid grid-cols-2 gap-4">
{(["top", "right", "bottom", "left"] as const).map((side) => (
<div key={side} className="space-y-1">
<Label htmlFor={id} className="capitalize">
{side}
</Label>
<Combobox items={fruits}>
<div className="flex items-center gap-0.5 border border-gray-6 rounded-sm px-2 py-1.5 focus-within:ring-2 focus-within:ring-gray-8">
<ComboboxInput
id={id}
placeholder="e.g. Apple"
className="flex-1 outline-none bg-transparent"
/>
<div className="flex items-center gap-0.5">
<ComboboxClear
className="p-0.5 hover:bg-accent-3 rounded-sm cursor-pointer"
aria-label="Clear selection"
>
<Cross2Icon className="size-1" />
</ComboboxClear>
<ComboboxTrigger
className="p-0.5 hover:bg-accent-3 rounded-sm cursor-pointer"
aria-label="Open popup"
>
<ChevronDownIcon className="size-1" />
</ComboboxTrigger>
</div>
</div>
<ComboboxPortal>
<ComboboxPositioner side={side} sideOffset={4}>
<ComboboxPopup className="w-40">
<ComboboxEmpty>No fruits found.</ComboboxEmpty>
<ComboboxList>
{(item: string) => (
<ComboboxItem key={item} value={item}>
<ComboboxItemIndicator>
<CheckIcon className="size-1" />
</ComboboxItemIndicator>
<span>{item}</span>
</ComboboxItem>
)}
</ComboboxList>
</ComboboxPopup>
</ComboboxPositioner>
</ComboboxPortal>
</Combobox>
</div>
))}
</div>
);
};