Dice UI
Components

Combobox

An input with a popover that helps users filter through a list of options.

"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.

PropTypeDefault
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 AttributeValue
[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.

PropTypeDefault
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.

PropTypeDefault
preventInputFocus?
boolean
false
asChild?
boolean
-
onResizeCapture?
ReactEventHandler<HTMLDivElement>
-
onResize?
ReactEventHandler<HTMLDivElement>
-
Data AttributeValue
[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.

PropTypeDefault
asChild?
boolean
-
onResizeCapture?
ReactEventHandler<HTMLButtonElement>
-
onResize?
ReactEventHandler<HTMLButtonElement>
-
Data AttributeValue
[data-state]"open" | "closed"
[data-disabled]Present when disabled

Input

The text input field that users can type into to filter options.

PropTypeDefault
asChild?
boolean
-
onResizeCapture?
ReactEventHandler<HTMLInputElement>
-
onResize?
ReactEventHandler<HTMLInputElement>
-

BadgeList

A container for displaying selected items as badges in a multi-select combobox.

PropTypeDefault
orientation?
"horizontal" | "vertical"
"horizontal"
forceMount?
boolean
false
asChild?
boolean
-
onResizeCapture?
ReactEventHandler<HTMLDivElement>
-
onResize?
ReactEventHandler<HTMLDivElement>
-
Data AttributeValue
[data-orientation]"horizontal" | "vertical"

BadgeItem

An individual badge representing a selected item in a multi-select combobox.

PropTypeDefault
disabled?
boolean
false
value
string
-
asChild?
boolean
-
onResizeCapture?
ReactEventHandler<HTMLDivElement>
-
onResize?
ReactEventHandler<HTMLDivElement>
-
Data AttributeValue
[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.

PropTypeDefault
asChild?
boolean
-
onResizeCapture?
ReactEventHandler<HTMLButtonElement>
-
onResize?
ReactEventHandler<HTMLButtonElement>
-
Data AttributeValue
[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.

PropTypeDefault
forceMount?
boolean
false
asChild?
boolean
-
onResizeCapture?
ReactEventHandler<HTMLButtonElement>
-
onResize?
ReactEventHandler<HTMLButtonElement>
-
Data AttributeValue
[data-disabled]Present when disabled

Portal

A portal for rendering the combobox content outside of its DOM hierarchy.

PropTypeDefault
container?
Element | DocumentFragment | null
document.body

Content

The popover container for combobox items. Positions the combobox popover relative to the anchor.

PropTypeDefault
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 AttributeValue
[data-state]"open" | "closed"
[data-side]"top" | "right" | "bottom" | "left"
[data-align]"start" | "center" | "end"
CSS VariableDescription
--dice-transform-originTransform origin for anchor positioning.
--dice-anchor-widthWidth of the anchor element.
--dice-anchor-heightHeight of the anchor element.
--dice-available-widthAvailable width in the viewport for the popover element.
--dice-available-heightAvailable height in the viewport for the popover element.

Arrow

A visual arrow element that points to the anchor.

PropTypeDefault
asChild?
boolean
-
onResizeCapture?
ReactEventHandler<SVGSVGElement>
-
onResize?
ReactEventHandler<SVGSVGElement>
-

Loading

A loading indicator for asynchronous filtering operations.

PropTypeDefault
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.

PropTypeDefault
keepVisible?
boolean
false
asChild?
boolean
-
onResizeCapture?
ReactEventHandler<HTMLDivElement>
-
onResize?
ReactEventHandler<HTMLDivElement>
-

Group

A container for logically grouping related options.

PropTypeDefault
forceMount?
boolean
false
asChild?
boolean
-
onResizeCapture?
ReactEventHandler<HTMLDivElement>
-
onResize?
ReactEventHandler<HTMLDivElement>
-

GroupLabel

A label that describes a group of options.

PropTypeDefault
asChild?
boolean
-
onResizeCapture?
ReactEventHandler<HTMLDivElement>
-
onResize?
ReactEventHandler<HTMLDivElement>
-

Item

An interactive item in the combobox list.

PropTypeDefault
disabled?
boolean
-
value
string
-
asChild?
boolean
-
onResizeCapture?
ReactEventHandler<HTMLDivElement>
-
onResize?
ReactEventHandler<HTMLDivElement>
-
onSelect?
((value: string) => void)
-
label?
string
-
Data AttributeValue
[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.

PropTypeDefault
asChild?
boolean
-
onResizeCapture?
ReactEventHandler<HTMLSpanElement>
-
onResize?
ReactEventHandler<HTMLSpanElement>
-

ItemIndicator

A visual indicator for selected options.

PropTypeDefault
forceMount?
boolean
false
asChild?
boolean
-
onResizeCapture?
ReactEventHandler<HTMLSpanElement>
-
onResize?
ReactEventHandler<HTMLSpanElement>
-

Separator

A visual divider for separating options or groups.

PropTypeDefault
keepVisible?
boolean
false
asChild?
boolean
-
onResizeCapture?
ReactEventHandler<HTMLDivElement>
-
onResize?
ReactEventHandler<HTMLDivElement>
-

Accessibility

Keyboard Interactions

KeyDescription
EnterWhen open, selects the highlighted option. When a badge is highlighted in multiple mode, removes the badge.
ArrowUpWhen open, highlights the previous option.
ArrowDownWhen open, highlights the next option.
ArrowLeftIn 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.
ArrowRightIn multiple mode: When a badge is highlighted, moves highlight to next badge. If on last badge, removes highlight and focuses input.
BackspaceDeleteIn multiple mode: When input is empty, removes the last badge. When a badge is highlighted, removes the highlighted badge.
HomeWhen open, highlights the first option.
EndWhen open, highlights the last option.
PageUpWhen open and modal is enabled, highlights the previous option.
PageDownWhen open and modal is enabled, highlights the next option.
EscapeCloses the combobox popover, returns focus to the input, and resets or restores the input value.