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

Installation with shadcn/ui

CLI

npx shadcn@latest add "https://diceui.com/r/combobox"

Manual

Install the following dependencies:

npm install @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 type * as React from "react";
 
import { cn } from "@/lib/utils";
 
const Combobox = (({
  className,
  ...props
}: React.ComponentProps<typeof ComboboxPrimitive.Root>) => {
  return (
    <ComboboxPrimitive.Root
      data-slot="combobox"
      className={cn(className)}
      {...props}
    />
  );
}) as ComboboxPrimitive.ComboboxRootComponentProps;
 
function ComboboxLabel({
  className,
  ...props
}: React.ComponentProps<typeof ComboboxPrimitive.Label>) {
  return (
    <ComboboxPrimitive.Label
      data-slot="combobox-label"
      className={cn("px-0.5 py-1.5 font-semibold text-sm", className)}
      {...props}
    />
  );
}
 
function ComboboxAnchor({
  className,
  ...props
}: React.ComponentProps<typeof ComboboxPrimitive.Anchor>) {
  return (
    <ComboboxPrimitive.Anchor
      data-slot="combobox-anchor"
      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}
    />
  );
}
 
function ComboboxInput({
  className,
  ...props
}: React.ComponentProps<typeof ComboboxPrimitive.Input>) {
  return (
    <ComboboxPrimitive.Input
      data-slot="combobox-input"
      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}
    />
  );
}
 
function ComboboxTrigger({
  className,
  children,
  ...props
}: React.ComponentProps<typeof ComboboxPrimitive.Trigger>) {
  return (
    <ComboboxPrimitive.Trigger
      data-slot="combobox-trigger"
      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="size-4" />}
    </ComboboxPrimitive.Trigger>
  );
}
 
function ComboboxCancel({
  className,
  ...props
}: React.ComponentProps<typeof ComboboxPrimitive.Cancel>) {
  return (
    <ComboboxPrimitive.Cancel
      data-slot="combobox-cancel"
      className={cn(
        "-translate-y-1/2 absolute top-1/2 right-1 flex size-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}
    />
  );
}
 
function ComboboxBadgeList({
  className,
  ...props
}: React.ComponentProps<typeof ComboboxPrimitive.BadgeList>) {
  return (
    <ComboboxPrimitive.BadgeList
      data-slot="combobox-badge-list"
      className={cn("flex flex-wrap items-center gap-1.5", className)}
      {...props}
    />
  );
}
 
function ComboboxBadgeItem({
  className,
  children,
  ...props
}: React.ComponentProps<typeof ComboboxPrimitive.BadgeItem>) {
  return (
    <ComboboxPrimitive.BadgeItem
      data-slot="combobox-badge-item"
      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="size-3" />
      </ComboboxPrimitive.BadgeItemDelete>
    </ComboboxPrimitive.BadgeItem>
  );
}
 
function ComboboxContent({
  sideOffset = 6,
  className,
  children,
  ...props
}: React.ComponentProps<typeof ComboboxPrimitive.Content>) {
  return (
    <ComboboxPrimitive.Portal>
      <ComboboxPrimitive.Content
        data-slot="combobox-content"
        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>
  );
}
 
function ComboboxLoading({
  className,
  ...props
}: React.ComponentProps<typeof ComboboxPrimitive.Loading>) {
  return (
    <ComboboxPrimitive.Loading
      data-slot="combobox-loading"
      className={cn("py-6 text-center text-sm", className)}
      {...props}
    >
      Loading...
    </ComboboxPrimitive.Loading>
  );
}
 
function ComboboxEmpty({
  className,
  ...props
}: React.ComponentProps<typeof ComboboxPrimitive.Empty>) {
  return (
    <ComboboxPrimitive.Empty
      data-slot="combobox-empty"
      className={cn("py-6 text-center text-sm", className)}
      {...props}
    />
  );
}
 
function ComboboxGroup({
  className,
  ...props
}: React.ComponentProps<typeof ComboboxPrimitive.Group>) {
  return (
    <ComboboxPrimitive.Group
      data-slot="combobox-group"
      className={cn("overflow-hidden", className)}
      {...props}
    />
  );
}
 
function ComboboxGroupLabel({
  className,
  ...props
}: React.ComponentProps<typeof ComboboxPrimitive.GroupLabel>) {
  return (
    <ComboboxPrimitive.GroupLabel
      data-slot="combobox-group-label"
      className={cn(
        "px-2 py-1.5 font-semibold text-muted-foreground text-xs",
        className,
      )}
      {...props}
    />
  );
}
 
function ComboboxItem({
  className,
  children,
  outset,
  ...props
}: React.ComponentProps<typeof ComboboxPrimitive.Item> & {
  outset?: boolean;
}) {
  return (
    <ComboboxPrimitive.Item
      data-slot="combobox-item"
      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 size-3.5 items-center justify-center",
          outset ? "right-2" : "left-2",
        )}
      >
        <Check className="size-4" />
      </ComboboxPrimitive.ItemIndicator>
      <ComboboxPrimitive.ItemText>{children}</ComboboxPrimitive.ItemText>
    </ComboboxPrimitive.Item>
  );
}
 
function ComboboxSeparator({
  className,
  ...props
}: React.ComponentProps<typeof ComboboxPrimitive.Separator>) {
  return (
    <ComboboxPrimitive.Separator
      data-slot="combobox-separator"
      className={cn("-mx-1 my-1 h-px bg-muted", className)}
      {...props}
    />
  );
}
 
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 { ChevronDown } from "lucide-react";
import * as React from "react";
import {
  Combobox,
  ComboboxAnchor,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxGroup,
  ComboboxGroupLabel,
  ComboboxInput,
  ComboboxItem,
  ComboboxLabel,
  ComboboxSeparator,
  ComboboxTrigger,
} from "@/components/ui/combobox";
 
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 { ChevronDown } from "lucide-react";
import * as React from "react";
import {
  Combobox,
  ComboboxAnchor,
  ComboboxBadgeItem,
  ComboboxBadgeList,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxInput,
  ComboboxItem,
  ComboboxLabel,
  ComboboxTrigger,
} from "@/components/ui/combobox";
 
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 { ChevronDown } from "lucide-react";
import { matchSorter } from "match-sorter";
import * as React from "react";
import {
  Combobox,
  ComboboxAnchor,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxInput,
  ComboboxItem,
  ComboboxLabel,
  ComboboxTrigger,
} from "@/components/ui/combobox";
 
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 { ChevronDown } from "lucide-react";
import * as React from "react";
import {
  Combobox,
  ComboboxAnchor,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxInput,
  ComboboxItem,
  ComboboxLabel,
  ComboboxLoading,
  ComboboxTrigger,
} from "@/components/ui/combobox";
 
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 { useVirtualizer } from "@tanstack/react-virtual";
import { ChevronDown } from "lucide-react";
import * as React from "react";
import { useDeferredValue } from "react";
import {
  Combobox,
  ComboboxAnchor,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxInput,
  ComboboxItem,
  ComboboxLabel,
  ComboboxTrigger,
} from "@/components/ui/combobox";
 
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 { ChevronDown } from "lucide-react";
import * as React from "react";
import {
  Combobox,
  ComboboxAnchor,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxGroup,
  ComboboxGroupLabel,
  ComboboxInput,
  ComboboxItem,
  ComboboxTrigger,
} from "@/components/ui/combobox";
import {
  TagsInput,
  TagsInputInput,
  TagsInputItem,
} from "@/components/ui/tags-input";
 
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
defaultValue?
Value<Multiple>
-
value?
Value<Multiple>
-
onValueChange?
((value: Value<Multiple>) => void)
-
asChild?
boolean
-
open?
boolean
-
defaultOpen?
boolean
false
onOpenChange?
((open: boolean) => void)
-
inputValue?
string
-
onInputValueChange?
((value: string) => void)
-
onFilter?
((options: string[], inputValue: string) => string[])
-
autoHighlight?
boolean
false
disabled?
boolean
-
exactMatch?
boolean
false
manualFiltering?
boolean
false
loop?
boolean
false
modal?
boolean
false
multiple?
Multiple
false
openOnFocus?
boolean
false
preserveInputOnBlur?
boolean
false
readOnly?
boolean
false
required?
boolean
false
name?
string
-
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
-

Anchor

A wrapper element that positions the combobox popover relative to the input and trigger. Provides the reference point for popover positioning.

PropTypeDefault
asChild?
boolean
-
preventInputFocus?
boolean
false
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
-
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
-

BadgeList

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

PropTypeDefault
asChild?
boolean
-
forceMount?
boolean
false
orientation?
"horizontal" | "vertical"
"horizontal"
Data AttributeValue
[data-orientation]"horizontal" | "vertical"

BadgeItem

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

PropTypeDefault
asChild?
boolean
-
value
string
-
disabled?
boolean
false
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
-
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
asChild?
boolean
-
forceMount?
boolean
false
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
side?
Side
"bottom"
sideOffset?
number
4
align?
Align
"start"
alignOffset?
number
0
collisionBoundary?
Boundary
-
collisionPadding?
number | Partial<Record<Side, number>>
0
arrowPadding?
number
0
sticky?
"partial" | "always"
"partial"
strategy?
Strategy
"absolute"
avoidCollisions?
boolean
true
fitViewport?
boolean
false
forceMount?
boolean
false
hideWhenDetached?
boolean
false
trackAnchor?
boolean
true
asChild?
boolean
-
onEscapeKeyDown?
((event: KeyboardEvent) => void)
-
onPointerDownOutside?
((event: PointerDownOutsideEvent) => void)
-
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
-

Loading

A loading indicator for asynchronous filtering operations.

PropTypeDefault
label?
string
-
asChild?
boolean
-
value?
number | null
null
max?
number
100

Empty

A placeholder component displayed when no options match the current filter.

PropTypeDefault
asChild?
boolean
-
keepVisible?
boolean
false

Group

A container for logically grouping related options.

PropTypeDefault
asChild?
boolean
-
forceMount?
boolean
false

GroupLabel

A label that describes a group of options.

PropTypeDefault
asChild?
boolean
-

Item

An interactive item in the combobox list.

PropTypeDefault
label?
string
-
onSelect?
((value: string) => void)
-
asChild?
boolean
-
value
string
-
disabled?
boolean
-
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
-

ItemIndicator

A visual indicator for selected options.

PropTypeDefault
asChild?
boolean
-
forceMount?
boolean
false

Separator

A visual divider for separating options or groups.

PropTypeDefault
asChild?
boolean
-
keepVisible?
boolean
false

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.