"use client";
import * as Combobox from "@diceui/combobox";
import { Check, ChevronDown } from "lucide-react";
import * as React from "react";
const tricks = [
{ label: "Kickflip", value: "kickflip" },
{ label: "Heelflip", value: "heelflip" },
{ label: "Tre Flip", value: "tre-flip" },
{ label: "FS 540", value: "fs-540" },
{ label: "Casper flip 360 flip", value: "casper-flip-360-flip" },
{ label: "Kickflip Backflip", value: "kickflip-backflip" },
{ label: "360 Varial McTwist", value: "360-varial-mc-twist" },
{ label: "The 900", value: "the-900" },
];
export function ComboboxDemo() {
return (
<Combobox.Root>
<Combobox.Label className="font-medium text-sm text-zinc-950 leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 dark:text-zinc-50">
Trick
</Combobox.Label>
<Combobox.Anchor className="flex h-9 w-full items-center justify-between rounded-md border border-zinc-200 bg-white px-3 py-2 shadow-xs transition-colors data-focused:ring-1 data-focused:ring-zinc-800 dark:border-zinc-800 dark:bg-zinc-950 dark:data-focused:ring-zinc-300">
<Combobox.Input
placeholder="Search trick..."
className="flex h-9 w-full rounded-md bg-transparent text-base text-zinc-900 placeholder:text-zinc-500 focus:outline-hidden disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:text-zinc-50 dark:placeholder:text-zinc-400"
/>
<Combobox.Trigger className="flex shrink-0 items-center justify-center rounded-r-md border-zinc-200 bg-transparent text-zinc-500 transition-colors hover:text-zinc-900 focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50 dark:border-zinc-800 dark:text-zinc-400 dark:hover:text-zinc-50">
<ChevronDown className="h-4 w-4" />
</Combobox.Trigger>
</Combobox.Anchor>
<Combobox.Portal>
<Combobox.Content className="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 min-w-[var(--dice-anchor-width)] overflow-hidden rounded-md border border-zinc-200 bg-white p-1 text-zinc-950 shadow-md data-[state=closed]:animate-out data-[state=open]:animate-in dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-50">
<Combobox.Empty className="py-6 text-center text-sm text-zinc-500 dark:text-zinc-400">
No tricks found.
</Combobox.Empty>
{tricks.map((trick) => (
<Combobox.Item
key={trick.value}
value={trick.value}
className="relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden data-disabled:pointer-events-none data-highlighted:bg-zinc-100 data-highlighted:text-zinc-900 data-disabled:opacity-50 dark:data-highlighted:bg-zinc-800 dark:data-highlighted:text-zinc-50"
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<Combobox.ItemIndicator>
<Check className="h-4 w-4" />
</Combobox.ItemIndicator>
</span>
<Combobox.ItemText>{trick.label}</Combobox.ItemText>
</Combobox.Item>
))}
</Combobox.Content>
</Combobox.Portal>
</Combobox.Root>
);
}
Installation
npm install @diceui/combobox
pnpm add @diceui/combobox
yarn add @diceui/combobox
bun add @diceui/combobox
Installation with shadcn/ui
CLI
npx shadcn@latest add "https://diceui.com/r/combobox"
pnpm dlx shadcn@latest add "https://diceui.com/r/combobox"
yarn dlx shadcn@latest add "https://diceui.com/r/combobox"
bun x shadcn@latest add "https://diceui.com/r/combobox"
Manual
Install the following dependencies:
npm install @diceui/combobox
pnpm add @diceui/combobox
yarn add @diceui/combobox
bun add @diceui/combobox
Copy and paste the following code into your project.
import * as ComboboxPrimitive from "@diceui/combobox";
import { Check, ChevronDown, X } from "lucide-react";
import * as React from "react";
import { cn } from "@/lib/utils";
const Combobox = React.forwardRef<
React.ComponentRef<typeof ComboboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ComboboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<ComboboxPrimitive.Root
data-slot="combobox"
ref={ref}
className={cn(className)}
{...props}
/>
)) as ComboboxPrimitive.ComboboxRootComponentProps;
Combobox.displayName = ComboboxPrimitive.Root.displayName;
const ComboboxLabel = React.forwardRef<
React.ComponentRef<typeof ComboboxPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof ComboboxPrimitive.Label>
>(({ className, ...props }, ref) => (
<ComboboxPrimitive.Label
data-slot="combobox-label"
ref={ref}
className={cn("px-0.5 py-1.5 font-semibold text-sm", className)}
{...props}
/>
));
ComboboxLabel.displayName = ComboboxPrimitive.Label.displayName;
const ComboboxAnchor = React.forwardRef<
React.ComponentRef<typeof ComboboxPrimitive.Anchor>,
React.ComponentPropsWithoutRef<typeof ComboboxPrimitive.Anchor>
>(({ className, ...props }, ref) => (
<ComboboxPrimitive.Anchor
data-slot="combobox-anchor"
ref={ref}
className={cn(
"relative flex h-9 w-full items-center justify-between gap-2 rounded-md border border-input bg-transparent px-3 py-2 shadow-xs data-focused:ring-1 data-focused:ring-ring",
className,
)}
{...props}
/>
));
ComboboxAnchor.displayName = ComboboxPrimitive.Anchor.displayName;
const ComboboxInput = React.forwardRef<
React.ComponentRef<typeof ComboboxPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof ComboboxPrimitive.Input>
>(({ className, ...props }, ref) => (
<ComboboxPrimitive.Input
data-slot="combobox-input"
ref={ref}
className={cn(
"flex h-9 w-full rounded-md bg-transparent text-base placeholder:text-muted-foreground focus:outline-hidden disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className,
)}
{...props}
/>
));
ComboboxInput.displayName = ComboboxPrimitive.Input.displayName;
const ComboboxTrigger = React.forwardRef<
React.ComponentRef<typeof ComboboxPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof ComboboxPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<ComboboxPrimitive.Trigger
data-slot="combobox-trigger"
ref={ref}
className={cn(
"flex shrink-0 items-center justify-center rounded-r-md border-input bg-transparent text-muted-foreground transition-colors hover:text-foreground/80 focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
>
{children || <ChevronDown className="h-4 w-4" />}
</ComboboxPrimitive.Trigger>
));
ComboboxTrigger.displayName = ComboboxPrimitive.Trigger.displayName;
const ComboboxCancel = React.forwardRef<
React.ComponentRef<typeof ComboboxPrimitive.Cancel>,
React.ComponentPropsWithoutRef<typeof ComboboxPrimitive.Cancel>
>(({ className, ...props }, ref) => (
<ComboboxPrimitive.Cancel
data-slot="combobox-cancel"
ref={ref}
className={cn(
"-translate-y-1/2 absolute top-1/2 right-1 flex h-6 w-6 items-center justify-center rounded-sm bg-background opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none",
className,
)}
{...props}
/>
));
ComboboxCancel.displayName = ComboboxPrimitive.Cancel.displayName;
const ComboboxBadgeList = React.forwardRef<
React.ComponentRef<typeof ComboboxPrimitive.BadgeList>,
React.ComponentPropsWithoutRef<typeof ComboboxPrimitive.BadgeList>
>(({ className, ...props }, ref) => (
<ComboboxPrimitive.BadgeList
data-slot="combobox-badge-list"
ref={ref}
className={cn("flex flex-wrap items-center gap-1.5", className)}
{...props}
/>
));
ComboboxBadgeList.displayName = ComboboxPrimitive.BadgeList.displayName;
const ComboboxBadgeItem = React.forwardRef<
React.ComponentRef<typeof ComboboxPrimitive.BadgeItem>,
React.ComponentPropsWithoutRef<typeof ComboboxPrimitive.BadgeItem>
>(({ className, children, ...props }, ref) => (
<ComboboxPrimitive.BadgeItem
data-slot="combobox-badge-item"
ref={ref}
className={cn(
"inline-flex items-center justify-between gap-1 rounded-sm bg-secondary px-2 py-0.5",
className,
)}
{...props}
>
<span className="truncate text-[13px] text-secondary-foreground">
{children}
</span>
<ComboboxPrimitive.BadgeItemDelete
data-slot="combobox-badge-item-delete"
className="shrink-0 rounded p-0.5 opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring data-highlighted:bg-destructive"
>
<X className="h-3 w-3" />
</ComboboxPrimitive.BadgeItemDelete>
</ComboboxPrimitive.BadgeItem>
));
ComboboxBadgeItem.displayName = ComboboxPrimitive.BadgeItem.displayName;
const ComboboxContent = React.forwardRef<
React.ComponentRef<typeof ComboboxPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof ComboboxPrimitive.Content>
>(({ sideOffset = 6, className, children, ...props }, ref) => (
<ComboboxPrimitive.Portal>
<ComboboxPrimitive.Content
data-slot="combobox-content"
ref={ref}
sideOffset={sideOffset}
className={cn(
"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-fit min-w-[var(--dice-anchor-width)] origin-[var(--dice-transform-origin)] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=closed]:animate-out data-[state=open]:animate-in",
className,
)}
{...props}
>
{children}
</ComboboxPrimitive.Content>
</ComboboxPrimitive.Portal>
));
ComboboxContent.displayName = ComboboxPrimitive.Content.displayName;
const ComboboxLoading = React.forwardRef<
React.ComponentRef<typeof ComboboxPrimitive.Loading>,
React.ComponentPropsWithoutRef<typeof ComboboxPrimitive.Loading>
>(({ className, ...props }, ref) => (
<ComboboxPrimitive.Loading
data-slot="combobox-loading"
ref={ref}
className={cn("py-6 text-center text-sm", className)}
{...props}
>
Loading...
</ComboboxPrimitive.Loading>
));
ComboboxLoading.displayName = ComboboxPrimitive.Loading.displayName;
const ComboboxEmpty = React.forwardRef<
React.ComponentRef<typeof ComboboxPrimitive.Empty>,
React.ComponentPropsWithoutRef<typeof ComboboxPrimitive.Empty>
>(({ className, ...props }, ref) => (
<ComboboxPrimitive.Empty
data-slot="combobox-empty"
ref={ref}
className={cn("py-6 text-center text-sm", className)}
{...props}
/>
));
ComboboxEmpty.displayName = ComboboxPrimitive.Empty.displayName;
const ComboboxGroup = React.forwardRef<
React.ComponentRef<typeof ComboboxPrimitive.Group>,
React.ComponentPropsWithoutRef<typeof ComboboxPrimitive.Group>
>(({ className, ...props }, ref) => (
<ComboboxPrimitive.Group
data-slot="combobox-group"
ref={ref}
className={cn("overflow-hidden", className)}
{...props}
/>
));
ComboboxGroup.displayName = ComboboxPrimitive.Group.displayName;
const ComboboxGroupLabel = React.forwardRef<
React.ComponentRef<typeof ComboboxPrimitive.GroupLabel>,
React.ComponentPropsWithoutRef<typeof ComboboxPrimitive.GroupLabel>
>(({ className, ...props }, ref) => (
<ComboboxPrimitive.GroupLabel
data-slot="combobox-group-label"
ref={ref}
className={cn(
"px-2 py-1.5 font-semibold text-muted-foreground text-xs",
className,
)}
{...props}
/>
));
ComboboxGroupLabel.displayName = ComboboxPrimitive.GroupLabel.displayName;
const ComboboxItem = React.forwardRef<
React.ComponentRef<typeof ComboboxPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof ComboboxPrimitive.Item> & {
outset?: boolean;
}
>(({ className, children, outset, ...props }, ref) => (
<ComboboxPrimitive.Item
data-slot="combobox-item"
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 text-sm outline-hidden data-disabled:pointer-events-none data-highlighted:bg-accent data-highlighted:text-accent-foreground data-disabled:opacity-50",
outset ? "pr-8 pl-2" : "pr-2 pl-8",
className,
)}
{...props}
>
<ComboboxPrimitive.ItemIndicator
className={cn(
"absolute flex h-3.5 w-3.5 items-center justify-center",
outset ? "right-2" : "left-2",
)}
>
<Check className="h-4 w-4" />
</ComboboxPrimitive.ItemIndicator>
<ComboboxPrimitive.ItemText>{children}</ComboboxPrimitive.ItemText>
</ComboboxPrimitive.Item>
));
ComboboxItem.displayName = ComboboxPrimitive.Item.displayName;
const ComboboxSeparator = React.forwardRef<
React.ComponentRef<typeof ComboboxPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof ComboboxPrimitive.Separator>
>(({ className, ...props }, ref) => (
<ComboboxPrimitive.Separator
data-slot="combobox-separator"
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
));
ComboboxSeparator.displayName = ComboboxPrimitive.Separator.displayName;
export {
Combobox,
ComboboxAnchor,
ComboboxInput,
ComboboxTrigger,
ComboboxCancel,
ComboboxBadgeList,
ComboboxBadgeItem,
ComboboxContent,
ComboboxEmpty,
ComboboxGroup,
ComboboxGroupLabel,
ComboboxItem,
ComboboxLabel,
ComboboxLoading,
ComboboxSeparator,
};
Layout
Import the parts, and compose them together.
import * as Combobox from "@diceui/combobox";
<Combobox.Root>
<Combobox.Label />
<Combobox.Anchor>
<Combobox.BadgeList>
<Combobox.BadgeItem>
<Combobox.BadgeItemDelete />
</Combobox.BadgeItem>
</Combobox.BadgeList>
<Combobox.Input />
<Combobox.Trigger />
<Combobox.Cancel />
</Combobox.Anchor>
<Combobox.Portal>
<Combobox.Content>
<Combobox.Arrow />
<Combobox.Loading />
<Combobox.Empty />
<Combobox.Group>
<Combobox.GroupLabel />
<Combobox.Item>
<Combobox.ItemText />
<Combobox.ItemIndicator />
</Combobox.Item>
</Combobox.Group>
<Combobox.Separator />
</Combobox.Content>
</Combobox.Portal>
</Combobox.Root>
Examples
With Groups
"use client";
import {
Combobox,
ComboboxAnchor,
ComboboxContent,
ComboboxEmpty,
ComboboxGroup,
ComboboxGroupLabel,
ComboboxInput,
ComboboxItem,
ComboboxLabel,
ComboboxSeparator,
ComboboxTrigger,
} from "@/components/ui/combobox";
import { ChevronDown } from "lucide-react";
import * as React from "react";
const tricks = [
{ label: "Kickflip", value: "kickflip" },
{ label: "Heelflip", value: "heelflip" },
{ label: "Tre Flip", value: "tre-flip" },
{ label: "FS 540", value: "fs-540" },
{ label: "Casper flip 360 flip", value: "casper-flip-360-flip" },
{ label: "Kickflip Backflip", value: "kickflip-backflip" },
{ label: "360 Varial McTwist", value: "360-varial-mc-twist" },
{ label: "The 900", value: "the-900" },
];
const groupedTricks = {
"Basic Tricks": tricks.slice(0, 3),
"Advanced Tricks": tricks.slice(3, 5),
"Pro Tricks": tricks.slice(5),
};
export function ComboboxGroupsDemo() {
const [value, setValue] = React.useState("");
return (
<Combobox value={value} onValueChange={setValue}>
<ComboboxLabel>Trick</ComboboxLabel>
<ComboboxAnchor>
<ComboboxInput placeholder="Select trick..." />
<ComboboxTrigger>
<ChevronDown className="h-4 w-4" />
</ComboboxTrigger>
</ComboboxAnchor>
<ComboboxContent>
<ComboboxEmpty>No tricks found</ComboboxEmpty>
{Object.entries(groupedTricks).map(([category, items], index) => (
<React.Fragment key={category}>
<ComboboxGroup>
<ComboboxGroupLabel>{category}</ComboboxGroupLabel>
{items.map((trick) => (
<ComboboxItem key={trick.value} value={trick.value} outset>
{trick.label}
</ComboboxItem>
))}
</ComboboxGroup>
{index < Object.entries(groupedTricks).length - 1 && (
<ComboboxSeparator />
)}
</React.Fragment>
))}
</ComboboxContent>
</Combobox>
);
}
With Multiple Selection
"use client";
import {
Combobox,
ComboboxAnchor,
ComboboxBadgeItem,
ComboboxBadgeList,
ComboboxContent,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxLabel,
ComboboxTrigger,
} from "@/components/ui/combobox";
import { ChevronDown } from "lucide-react";
import * as React from "react";
const tricks = [
{ label: "Kickflip", value: "kickflip" },
{ label: "Heelflip", value: "heelflip" },
{ label: "Tre Flip", value: "tre-flip" },
{ label: "FS 540", value: "fs-540" },
{ label: "Casper flip 360 flip", value: "casper-flip-360-flip" },
{ label: "Kickflip Backflip", value: "kickflip-backflip" },
{ label: "360 Varial McTwist", value: "360-varial-mc-twist" },
{ label: "The 900", value: "the-900" },
];
export function ComboboxMultipleDemo() {
const [value, setValue] = React.useState<string[]>([]);
return (
<Combobox
value={value}
onValueChange={setValue}
className="w-[400px]"
multiple
autoHighlight
>
<ComboboxLabel>Tricks</ComboboxLabel>
<ComboboxAnchor className="h-full min-h-10 flex-wrap px-3 py-2">
<ComboboxBadgeList>
{value.map((item) => {
const option = tricks.find((trick) => trick.value === item);
if (!option) return null;
return (
<ComboboxBadgeItem key={item} value={item}>
{option.label}
</ComboboxBadgeItem>
);
})}
</ComboboxBadgeList>
<ComboboxInput
placeholder="Select tricks..."
className="h-auto min-w-20 flex-1"
/>
<ComboboxTrigger className="absolute top-3 right-2">
<ChevronDown className="h-4 w-4" />
</ComboboxTrigger>
</ComboboxAnchor>
<ComboboxContent>
<ComboboxEmpty>No tricks found.</ComboboxEmpty>
{tricks.map((trick) => (
<ComboboxItem key={trick.value} value={trick.value}>
{trick.label}
</ComboboxItem>
))}
</ComboboxContent>
</Combobox>
);
}
With Custom Filter
"use client";
import {
Combobox,
ComboboxAnchor,
ComboboxContent,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxLabel,
ComboboxTrigger,
} from "@/components/ui/combobox";
import { ChevronDown } from "lucide-react";
import { matchSorter } from "match-sorter";
import * as React from "react";
const tricks = [
{ label: "Kickflip", value: "kickflip" },
{ label: "Heelflip", value: "heelflip" },
{ label: "Tre Flip", value: "tre-flip" },
{ label: "FS 540", value: "fs-540" },
{ label: "Casper flip 360 flip", value: "casper-flip-360-flip" },
{ label: "Kickflip Backflip", value: "kickflip-backflip" },
{ label: "360 Varial McTwist", value: "360-varial-mc-twist" },
{ label: "The 900", value: "the-900" },
];
export function ComboboxCustomFilterDemo() {
const [value, setValue] = React.useState("");
function onFilter(options: string[], inputValue: string) {
const trickOptions = tricks.filter((trick) =>
options.includes(trick.value),
);
return matchSorter(trickOptions, inputValue, {
keys: ["label", "value"],
threshold: matchSorter.rankings.MATCHES,
}).map((trick) => trick.value);
}
return (
<Combobox value={value} onValueChange={setValue} onFilter={onFilter}>
<ComboboxLabel>Trick</ComboboxLabel>
<ComboboxAnchor>
<ComboboxInput placeholder="Select trick..." />
<ComboboxTrigger>
<ChevronDown className="h-4 w-4" />
</ComboboxTrigger>
</ComboboxAnchor>
<ComboboxContent>
<ComboboxEmpty>No tricks found.</ComboboxEmpty>
{tricks.map((trick) => (
<ComboboxItem key={trick.value} value={trick.value}>
{trick.label}
</ComboboxItem>
))}
</ComboboxContent>
</Combobox>
);
}
With Debounce
"use client";
import {
Combobox,
ComboboxAnchor,
ComboboxContent,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxLabel,
ComboboxLoading,
ComboboxTrigger,
} from "@/components/ui/combobox";
import { ChevronDown } from "lucide-react";
import * as React from "react";
const tricks = [
{ label: "Kickflip", value: "kickflip" },
{ label: "Heelflip", value: "heelflip" },
{ label: "Tre Flip", value: "tre-flip" },
{ label: "FS 540", value: "fs-540" },
{ label: "Casper flip 360 flip", value: "casper-flip-360-flip" },
{ label: "Kickflip Backflip", value: "kickflip-backflip" },
{ label: "360 Varial McTwist", value: "360-varial-mc-twist" },
{ label: "The 900", value: "the-900" },
];
export function ComboboxDebouncedDemo() {
const [value, setValue] = React.useState("");
const [search, setSearch] = React.useState("");
const [isLoading, setIsLoading] = React.useState(false);
const [progress, setProgress] = React.useState(0);
const [filteredItems, setFilteredItems] = React.useState(tricks);
// Debounce search with loading simulation
const debouncedSearch = React.useCallback(
debounce(async (searchTerm: string) => {
setIsLoading(true);
setProgress(0);
// Simulate a more realistic progress pattern
const progressSteps = [15, 35, 65, 85, 95] as const;
let currentStepIndex = 0;
const interval = setInterval(() => {
if (currentStepIndex < progressSteps.length) {
setProgress(progressSteps[currentStepIndex] ?? 0);
currentStepIndex++;
}
}, 150);
// Simulate API delay with variable timing
const delay = Math.random() * 300 + 400; // Random delay between 400-700ms
await new Promise((resolve) => setTimeout(resolve, delay));
const results = tricks.filter((trick) =>
trick.label.toLowerCase().includes(searchTerm.toLowerCase()),
);
setFilteredItems(results);
setProgress(100);
setIsLoading(false);
clearInterval(interval);
}, 300),
[],
);
const onInputValueChange = React.useCallback(
(value: string) => {
setSearch(value);
debouncedSearch(value);
},
[debouncedSearch],
);
return (
<Combobox
value={value}
onValueChange={setValue}
inputValue={search}
onInputValueChange={onInputValueChange}
manualFiltering
>
<ComboboxLabel>Trick</ComboboxLabel>
<ComboboxAnchor>
<ComboboxInput placeholder="Search trick..." />
<ComboboxTrigger>
<ChevronDown className="h-4 w-4" />
</ComboboxTrigger>
</ComboboxAnchor>
<ComboboxContent>
{isLoading ? (
<ComboboxLoading value={progress} label="Searching tricks..." />
) : null}
<ComboboxEmpty keepVisible={!isLoading && filteredItems.length === 0}>
No trick found.
</ComboboxEmpty>
{!isLoading &&
filteredItems.map((trick) => (
<ComboboxItem key={trick.value} value={trick.value} outset>
{trick.label}
</ComboboxItem>
))}
</ComboboxContent>
</Combobox>
);
}
function debounce<TFunction extends (...args: never[]) => unknown>(
func: TFunction,
wait: number,
): (...args: Parameters<TFunction>) => void {
let timeoutId: ReturnType<typeof setTimeout>;
return function (this: unknown, ...args: Parameters<TFunction>): void {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
func.apply(this, args);
}, wait);
};
}
With Virtualization
"use client";
import {
Combobox,
ComboboxAnchor,
ComboboxContent,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxLabel,
ComboboxTrigger,
} from "@/components/ui/combobox";
import { useVirtualizer } from "@tanstack/react-virtual";
import { ChevronDown } from "lucide-react";
import * as React from "react";
import { useDeferredValue } from "react";
interface Option {
label: string;
value: string;
}
const categories = [
"Flip",
"Grind",
"Slide",
"Grab",
"Manual",
"Transition",
"Old School",
] as const;
const variations = [
"Regular",
"Switch",
"Nollie",
"Fakie",
"360",
"Double",
"Late",
] as const;
type Category = (typeof categories)[number];
type Variation = (typeof variations)[number];
const generateItems = (count: number): Option[] => {
return Array.from({ length: count }, (_, i) => {
const category: Category = categories[i % categories.length] ?? "Flip";
const variation: Variation = variations[i % variations.length] ?? "Regular";
const trickNumber = Math.floor(i / categories.length) + 1;
return {
label: `${variation} ${category} ${trickNumber}`,
value: `trick-${i + 1}`,
};
});
};
const items = generateItems(10000);
export function ComboboxVirtualizedDemo() {
const [content, setContent] =
React.useState<React.ComponentRef<"div"> | null>(null);
const [value, setValue] = React.useState("");
const [inputValue, setInputValue] = React.useState("");
const deferredInputValue = useDeferredValue(inputValue);
const filteredTricks = React.useMemo(() => {
if (!deferredInputValue) return items;
const normalized = deferredInputValue.toLowerCase();
return items.filter((item) =>
item.label.toLowerCase().includes(normalized),
);
}, [deferredInputValue]);
const virtualizer = useVirtualizer({
count: filteredTricks.length,
getScrollElement: () => content,
estimateSize: () => 32,
overscan: 20,
});
const onInputValueChange = React.useCallback(
(value: string) => {
setInputValue(value);
if (content) {
content.scrollTop = 0; // Reset scroll position
virtualizer.measure();
}
},
[content, virtualizer],
);
// Re-measure virtualizer when filteredItems changes
React.useEffect(() => {
if (content) {
virtualizer.measure();
}
}, [content, virtualizer]);
return (
<Combobox
value={value}
onValueChange={setValue}
inputValue={inputValue}
onInputValueChange={onInputValueChange}
manualFiltering
>
<ComboboxLabel>Trick</ComboboxLabel>
<ComboboxAnchor>
<ComboboxInput placeholder="Search tricks..." />
<ComboboxTrigger>
<ChevronDown className="h-4 w-4" />
</ComboboxTrigger>
</ComboboxAnchor>
<ComboboxContent
ref={(node) => setContent(node)}
className="relative max-h-[300px] overflow-y-auto overflow-x-hidden"
>
<ComboboxEmpty>No tricks found.</ComboboxEmpty>
<div
className="relative w-full"
style={{
height: `${virtualizer.getTotalSize()}px`,
}}
>
{virtualizer.getVirtualItems().map((virtualItem) => {
const trick = filteredTricks[virtualItem.index];
if (!trick) return null;
return (
<ComboboxItem
key={virtualItem.key}
value={trick.value}
className="absolute top-0 left-0 w-full"
style={{
height: `${virtualItem.size}px`,
transform: `translateY(${virtualItem.start}px)`,
}}
outset
>
{trick.label}
</ComboboxItem>
);
})}
</div>
</ComboboxContent>
</Combobox>
);
}
With Tags Input
"use client";
import {
Combobox,
ComboboxAnchor,
ComboboxContent,
ComboboxEmpty,
ComboboxGroup,
ComboboxGroupLabel,
ComboboxInput,
ComboboxItem,
ComboboxTrigger,
} from "@/components/ui/combobox";
import {
TagsInput,
TagsInputInput,
TagsInputItem,
} from "@/components/ui/tags-input";
import { ChevronDown } from "lucide-react";
import * as React from "react";
const tricks = [
"Kickflip",
"Heelflip",
"Tre Flip",
"FS 540",
"Casper flip 360 flip",
"Kickflip Backflip",
"360 Varial McTwist",
"The 900",
];
export function ComboboxTagsDemo() {
const [value, setValue] = React.useState<string[]>([]);
return (
<Combobox value={value} onValueChange={setValue} multiple>
<ComboboxAnchor asChild>
<TagsInput
className="relative flex h-full min-h-10 w-[400px] flex-row flex-wrap items-center justify-start gap-1.5 px-2.5 py-2"
value={value}
onValueChange={setValue}
>
{value.map((item) => (
<TagsInputItem key={item} value={item}>
{item}
</TagsInputItem>
))}
<ComboboxInput className="h-fit flex-1 p-0" asChild>
<TagsInputInput placeholder="Tricks..." />
</ComboboxInput>
<ComboboxTrigger className="absolute top-2.5 right-2">
<ChevronDown className="h-4 w-4 text-muted-foreground" />
</ComboboxTrigger>
</TagsInput>
</ComboboxAnchor>
<ComboboxContent sideOffset={5}>
<ComboboxEmpty>No tricks found.</ComboboxEmpty>
<ComboboxGroup>
<ComboboxGroupLabel>Tricks</ComboboxGroupLabel>
{tricks.map((trick) => (
<ComboboxItem key={trick} value={trick} outset>
{trick}
</ComboboxItem>
))}
</ComboboxGroup>
</ComboboxContent>
</Combobox>
);
}
API Reference
Root
The container for all combobox parts.
Prop | Type | Default |
---|---|---|
name? | string | - |
required? | boolean | false |
readOnly? | boolean | false |
preserveInputOnBlur? | boolean | false |
openOnFocus? | boolean | false |
multiple? | Multiple | false |
modal? | boolean | false |
loop? | boolean | false |
manualFiltering? | boolean | false |
exactMatch? | boolean | false |
disabled? | boolean | - |
autoHighlight? | boolean | false |
onFilter? | ((options: string[], inputValue: string) => string[]) | - |
onInputValueChange? | ((value: string) => void) | - |
inputValue? | string | - |
onOpenChange? | ((open: boolean) => void) | - |
defaultOpen? | boolean | false |
open? | boolean | - |
asChild? | boolean | - |
onResizeCapture? | ReactEventHandler<HTMLDivElement> | - |
onResize? | ReactEventHandler<HTMLDivElement> | - |
onValueChange? | ((value: Value<Multiple>) => void) | - |
value? | Value<Multiple> | - |
defaultValue? | Value<Multiple> | - |
Data Attribute | Value |
---|---|
[data-state] | "open" | "closed" |
[data-disabled] | Present when disabled |
Label
An accessible label that describes the combobox. Associates with the input element for screen readers.
Prop | Type | Default |
---|---|---|
asChild? | boolean | - |
onResizeCapture? | ReactEventHandler<HTMLLabelElement> | - |
onResize? | ReactEventHandler<HTMLLabelElement> | - |
Anchor
A wrapper element that positions the combobox popover relative to the input and trigger. Provides the reference point for popover positioning.
Prop | Type | Default |
---|---|---|
preventInputFocus? | boolean | false |
asChild? | boolean | - |
onResizeCapture? | ReactEventHandler<HTMLDivElement> | - |
onResize? | ReactEventHandler<HTMLDivElement> | - |
Data Attribute | Value |
---|---|
[data-state] | "open" | "closed" |
[data-anchor] | Present when the anchor is present |
[data-disabled] | Present when disabled |
[data-focused] | Present when the anchor is focused |
Trigger
A button that toggles the combobox popover. Handles focus management and keyboard interactions for opening/closing the popover.
Prop | Type | Default |
---|---|---|
asChild? | boolean | - |
onResizeCapture? | ReactEventHandler<HTMLButtonElement> | - |
onResize? | ReactEventHandler<HTMLButtonElement> | - |
Data Attribute | Value |
---|---|
[data-state] | "open" | "closed" |
[data-disabled] | Present when disabled |
Input
The text input field that users can type into to filter options.
Prop | Type | Default |
---|---|---|
asChild? | boolean | - |
onResizeCapture? | ReactEventHandler<HTMLInputElement> | - |
onResize? | ReactEventHandler<HTMLInputElement> | - |
BadgeList
A container for displaying selected items as badges in a multi-select combobox.
Prop | Type | Default |
---|---|---|
orientation? | "horizontal" | "vertical" | "horizontal" |
forceMount? | boolean | false |
asChild? | boolean | - |
onResizeCapture? | ReactEventHandler<HTMLDivElement> | - |
onResize? | ReactEventHandler<HTMLDivElement> | - |
Data Attribute | Value |
---|---|
[data-orientation] | "horizontal" | "vertical" |
BadgeItem
An individual badge representing a selected item in a multi-select combobox.
Prop | Type | Default |
---|---|---|
disabled? | boolean | false |
value | string | - |
asChild? | boolean | - |
onResizeCapture? | ReactEventHandler<HTMLDivElement> | - |
onResize? | ReactEventHandler<HTMLDivElement> | - |
Data Attribute | Value |
---|---|
[data-disabled] | Present when the badge is disabled |
[data-highlighted] | Present when the badge is highlighted |
[data-orientation] | "horizontal" | "vertical" |
BadgeItemDelete
A button to remove a selected item from the multi-select combobox.
Prop | Type | Default |
---|---|---|
asChild? | boolean | - |
onResizeCapture? | ReactEventHandler<HTMLButtonElement> | - |
onResize? | ReactEventHandler<HTMLButtonElement> | - |
Data Attribute | Value |
---|---|
[data-disabled] | Present when the parent badge is disabled |
[data-highlighted] | Present when the parent badge is highlighted |
Cancel
A button that clears the input value and resets the filter.
Prop | Type | Default |
---|---|---|
forceMount? | boolean | false |
asChild? | boolean | - |
onResizeCapture? | ReactEventHandler<HTMLButtonElement> | - |
onResize? | ReactEventHandler<HTMLButtonElement> | - |
Data Attribute | Value |
---|---|
[data-disabled] | Present when disabled |
Portal
A portal for rendering the combobox content outside of its DOM hierarchy.
Prop | Type | Default |
---|---|---|
container? | Element | DocumentFragment | null | document.body |
Content
The popover container for combobox items. Positions the combobox popover relative to the anchor.
Prop | Type | Default |
---|---|---|
onPointerDownOutside? | ((event: PointerDownOutsideEvent) => void) | - |
onEscapeKeyDown? | ((event: KeyboardEvent) => void) | - |
asChild? | boolean | - |
onResizeCapture? | ReactEventHandler<HTMLDivElement> | - |
onResize? | ReactEventHandler<HTMLDivElement> | - |
trackAnchor? | boolean | true |
hideWhenDetached? | boolean | false |
forceMount? | boolean | false |
fitViewport? | boolean | false |
avoidCollisions? | boolean | true |
strategy? | Strategy | "absolute" |
sticky? | "partial" | "always" | "partial" |
arrowPadding? | number | 0 |
collisionPadding? | number | Partial<Record<Side, number>> | 0 |
collisionBoundary? | Boundary | - |
alignOffset? | number | 0 |
align? | Align | "start" |
sideOffset? | number | 4 |
side? | Side | "bottom" |
Data Attribute | Value |
---|---|
[data-state] | "open" | "closed" |
[data-side] | "top" | "right" | "bottom" | "left" |
[data-align] | "start" | "center" | "end" |
CSS Variable | Description |
---|---|
--dice-transform-origin | Transform origin for anchor positioning. |
--dice-anchor-width | Width of the anchor element. |
--dice-anchor-height | Height of the anchor element. |
--dice-available-width | Available width in the viewport for the popover element. |
--dice-available-height | Available height in the viewport for the popover element. |
Arrow
A visual arrow element that points to the anchor.
Prop | Type | Default |
---|---|---|
asChild? | boolean | - |
onResizeCapture? | ReactEventHandler<SVGSVGElement> | - |
onResize? | ReactEventHandler<SVGSVGElement> | - |
Loading
A loading indicator for asynchronous filtering operations.
Prop | Type | Default |
---|---|---|
max? | number | 100 |
value? | number | null | null |
asChild? | boolean | - |
onResizeCapture? | ReactEventHandler<HTMLDivElement> | - |
onResize? | ReactEventHandler<HTMLDivElement> | - |
label? | string | - |
Empty
A placeholder component displayed when no options match the current filter.
Prop | Type | Default |
---|---|---|
keepVisible? | boolean | false |
asChild? | boolean | - |
onResizeCapture? | ReactEventHandler<HTMLDivElement> | - |
onResize? | ReactEventHandler<HTMLDivElement> | - |
Group
A container for logically grouping related options.
Prop | Type | Default |
---|---|---|
forceMount? | boolean | false |
asChild? | boolean | - |
onResizeCapture? | ReactEventHandler<HTMLDivElement> | - |
onResize? | ReactEventHandler<HTMLDivElement> | - |
GroupLabel
A label that describes a group of options.
Prop | Type | Default |
---|---|---|
asChild? | boolean | - |
onResizeCapture? | ReactEventHandler<HTMLDivElement> | - |
onResize? | ReactEventHandler<HTMLDivElement> | - |
Item
An interactive item in the combobox list.
Prop | Type | Default |
---|---|---|
disabled? | boolean | - |
value | string | - |
asChild? | boolean | - |
onResizeCapture? | ReactEventHandler<HTMLDivElement> | - |
onResize? | ReactEventHandler<HTMLDivElement> | - |
onSelect? | ((value: string) => void) | - |
label? | string | - |
Data Attribute | Value |
---|---|
[data-highlighted] | Present when the item is highlighted |
[data-disabled] | Present when the item is disabled |
[data-state] | "checked" | "unchecked" |
ItemText
The textual content of an item.
Prop | Type | Default |
---|---|---|
asChild? | boolean | - |
onResizeCapture? | ReactEventHandler<HTMLSpanElement> | - |
onResize? | ReactEventHandler<HTMLSpanElement> | - |
ItemIndicator
A visual indicator for selected options.
Prop | Type | Default |
---|---|---|
forceMount? | boolean | false |
asChild? | boolean | - |
onResizeCapture? | ReactEventHandler<HTMLSpanElement> | - |
onResize? | ReactEventHandler<HTMLSpanElement> | - |
Separator
A visual divider for separating options or groups.
Prop | Type | Default |
---|---|---|
keepVisible? | boolean | false |
asChild? | boolean | - |
onResizeCapture? | ReactEventHandler<HTMLDivElement> | - |
onResize? | ReactEventHandler<HTMLDivElement> | - |
Accessibility
Keyboard Interactions
Key | Description |
---|---|
Enter | When open, selects the highlighted option. When a badge is highlighted in multiple mode, removes the badge. |
ArrowUp | When open, highlights the previous option. |
ArrowDown | When open, highlights the next option. |
ArrowLeft | In multiple mode: When cursor is at start of input, closes the menu and highlights the last badge. When a badge is highlighted, moves highlight to previous badge. |
ArrowRight | In multiple mode: When a badge is highlighted, moves highlight to next badge. If on last badge, removes highlight and focuses input. |
BackspaceDelete | In multiple mode: When input is empty, removes the last badge. When a badge is highlighted, removes the highlighted badge. |
Home | When open, highlights the first option. |
End | When open, highlights the last option. |
PageUp | When open and modal is enabled, highlights the previous option. |
PageDown | When open and modal is enabled, highlights the next option. |
Escape | Closes the combobox popover, returns focus to the input, and resets or restores the input value. |