Aura Design System

Combobox

A combobox component that allows users to search and select from a list of options.

Preview

Loading...
import { useState, useId } from "react";
import {
  Combobox,
  ComboboxInput,
  ComboboxTrigger,
  ComboboxClear,
  ComboboxValue,
  ComboboxPortal,
  ComboboxPositioner,
  ComboboxPopup,
  ComboboxList,
  ComboboxItem,
  ComboboxItemIndicator,
  ComboboxEmpty,
  ComboboxChips,
  ComboboxChip,
  ComboboxChipRemove,
  ComboboxGroup,
  ComboboxGroupLabel,
  ComboboxSeparator,
} from "@/components/ui/Combobox";
import { Button } from "@/components/ui/Button";
import { Label } from "@/components/ui/Label";
import { CheckIcon, Cross2Icon, ChevronDownIcon } from "@radix-ui/react-icons";

export function ComboboxDemo() {
  return {
  const id = useId();
  return (
    <div className="space-y-1">
      <Label htmlFor={id}>Choose a fruit</Label>
      <Combobox items={fruits}>
        <div className="flex items-center gap-0.5 border border-gray-6 rounded-sm px-2 py-1.5 focus-within:ring-2 focus-within:ring-gray-8">
          <ComboboxInput
            id={id}
            placeholder="e.g. Apple"
            className="flex-1 outline-none bg-transparent"
          />
          <div className="flex items-center gap-0.5">
            <ComboboxClear
              className="p-0.5 hover:bg-accent-3 rounded-sm cursor-pointer"
              aria-label="Clear selection"
            >
              <Cross2Icon className="size-1" />
            </ComboboxClear>
            <ComboboxTrigger
              className="p-0.5 hover:bg-accent-3 rounded-sm cursor-pointer"
              aria-label="Open popup"
            >
              <ChevronDownIcon className="size-1" />
            </ComboboxTrigger>
          </div>
        </div>
        <ComboboxPortal>
          <ComboboxPositioner sideOffset={4}>
            <ComboboxPopup className="w-40">
              <ComboboxEmpty>No fruits found.</ComboboxEmpty>
              <ComboboxList>
                {(item: string) => (
                  <ComboboxItem key={item} value={item}>
                    <ComboboxItemIndicator>
                      <CheckIcon className="size-1" />
                    </ComboboxItemIndicator>
                    <span>{item}</span>
                  </ComboboxItem>
                )}
              </ComboboxList>
            </ComboboxPopup>
          </ComboboxPositioner>
        </ComboboxPortal>
      </Combobox>
    </div>
  );
};

Installation

Make sure that namespace is set in your component.json file. Namespace docs: Learn more about namespaces

pnpm dlx shadcn@latest add @aura/combobox

Manual

Install the following dependencies:

pnpm install @base-ui/react

Copy and paste the class names utility into your utils/class-names.ts file.

import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}

Copy and paste the Combobox component into your components/ui/Combobox.tsx file.

"use client";

/**
 * @description A combobox component that allows users to search and select from a list of options.
 */
import * as React from "react";
import { Combobox } from "@base-ui/react/combobox";

import { cn } from "@/utils/class-names";

const ComboboxRoot = Combobox.Root;

function ComboboxInput({
  className,
  ...props
}: React.ComponentProps<typeof Combobox.Input>) {
  return (
    <Combobox.Input
      data-slot="combobox-input"
      className={cn(className)}
      {...props}
    />
  );
}

function ComboboxTrigger({
  className,
  ...props
}: React.ComponentProps<typeof Combobox.Trigger>) {
  return (
    <Combobox.Trigger
      data-slot="combobox-trigger"
      className={cn(className)}
      {...props}
    />
  );
}

function ComboboxIcon({
  className,
  ...props
}: React.ComponentProps<typeof Combobox.Icon>) {
  return (
    <Combobox.Icon
      data-slot="combobox-icon"
      className={cn(className)}
      {...props}
    />
  );
}

function ComboboxClear({
  className,
  ...props
}: React.ComponentProps<typeof Combobox.Clear>) {
  return (
    <Combobox.Clear
      data-slot="combobox-clear"
      className={cn(className)}
      {...props}
    />
  );
}

function ComboboxValue({
  ...props
}: React.ComponentProps<typeof Combobox.Value>) {
  return <Combobox.Value data-slot="combobox-value" {...props} />;
}

function ComboboxChips({
  className,
  ...props
}: React.ComponentProps<typeof Combobox.Chips>) {
  return (
    <Combobox.Chips
      data-slot="combobox-chips"
      className={cn(className)}
      {...props}
    />
  );
}

function ComboboxChip({
  className,
  ...props
}: React.ComponentProps<typeof Combobox.Chip>) {
  return (
    <Combobox.Chip
      data-slot="combobox-chip"
      className={cn(className)}
      {...props}
    />
  );
}

function ComboboxChipRemove({
  className,
  ...props
}: React.ComponentProps<typeof Combobox.ChipRemove>) {
  return (
    <Combobox.ChipRemove
      data-slot="combobox-chip-remove"
      className={cn(className)}
      {...props}
    />
  );
}

function ComboboxPortal({
  ...props
}: React.ComponentProps<typeof Combobox.Portal>) {
  return <Combobox.Portal data-slot="combobox-portal" {...props} />;
}

function ComboboxBackdrop({
  className,
  ...props
}: React.ComponentProps<typeof Combobox.Backdrop>) {
  return (
    <Combobox.Backdrop
      data-slot="combobox-backdrop"
      className={cn(className)}
      {...props}
    />
  );
}

function ComboboxPositioner({
  className,
  ...props
}: React.ComponentProps<typeof Combobox.Positioner>) {
  return (
    <Combobox.Positioner
      data-slot="combobox-positioner"
      collisionPadding={8}
      className={cn("outline-0", className)}
      {...props}
    />
  );
}

function ComboboxPopup({
  className,
  ...props
}: React.ComponentProps<typeof Combobox.Popup>) {
  return (
    <Combobox.Popup
      data-slot="combobox-popup"
      className={cn(
        "bg-gray-1 border border-gray-a6 rounded-sm relative shadow-md w-[var(--anchor-width)] max-h-[23rem] max-w-[var(--available-width)]",
        className
      )}
      {...props}
    />
  );
}

function ComboboxArrow({
  className,
  ...props
}: React.ComponentProps<typeof Combobox.Arrow>) {
  return (
    <Combobox.Arrow
      data-slot="combobox-arrow"
      className={cn(className, "fill-gray-1")}
      {...props}
    />
  );
}

function ComboboxStatus({
  className,
  ...props
}: React.ComponentProps<typeof Combobox.Status>) {
  return (
    <Combobox.Status
      data-slot="combobox-status"
      className={cn(className)}
      {...props}
    />
  );
}

function ComboboxEmpty({
  className,
  ...props
}: React.ComponentProps<typeof Combobox.Empty>) {
  return (
    <Combobox.Empty
      data-slot="combobox-empty"
      className={cn(
        "text-gray-11 py-1 px-2 text-sm text-center empty:hidden",
        className
      )}
      {...props}
    />
  );
}

function ComboboxList({
  className,
  ...props
}: React.ComponentProps<typeof Combobox.List>) {
  return (
    <Combobox.List
      data-slot="combobox-list"
      className={cn(
        "overflow-y-auto overscroll-contain py-0.5 scroll-py-0.5 outline-0 max-h-[min(23rem,var(--available-height))] data-[empty]:p-0",
        className
      )}
      {...props}
    />
  );
}

function ComboboxRow({
  className,
  ...props
}: React.ComponentProps<typeof Combobox.Row>) {
  return (
    <Combobox.Row
      data-slot="combobox-row"
      className={cn(className)}
      {...props}
    />
  );
}

function ComboboxItem({
  className,
  ...props
}: React.ComponentProps<typeof Combobox.Item>) {
  return (
    <Combobox.Item
      data-slot="combobox-item"
      className={cn(
        "relative flex w-full cursor-default select-none items-center rounded-sm p-0.5 px-2 text-sm outline-none hover:bg-accent-3 focus:bg-accent-3 data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
        className
      )}
      {...props}
    />
  );
}

function ComboboxItemIndicator({
  className,
  ...props
}: React.ComponentProps<typeof Combobox.ItemIndicator>) {
  return (
    <Combobox.ItemIndicator
      data-slot="combobox-item-indicator"
      className={cn(
        "absolute right-0.5 top-0 bottom-0 flex items-center justify-center",
        className
      )}
      {...props}
    />
  );
}

function ComboboxSeparator({
  className,
  ...props
}: React.ComponentProps<typeof Combobox.Separator>) {
  return (
    <Combobox.Separator
      data-slot="combobox-separator"
      className={cn("m-0.6 h-px bg-gray-a6", className)}
      {...props}
    />
  );
}

function ComboboxGroup({
  className,
  ...props
}: React.ComponentProps<typeof Combobox.Group>) {
  return (
    <Combobox.Group
      data-slot="combobox-group"
      className={cn(className)}
      {...props}
    />
  );
}

function ComboboxGroupLabel({
  className,
  ...props
}: React.ComponentProps<typeof Combobox.GroupLabel>) {
  return (
    <Combobox.GroupLabel
      data-slot="combobox-group-label"
      className={cn(
        "p-0.5 px-2 text-xs font-medium text-gray-12",
        className
      )}
      {...props}
    />
  );
}

function ComboboxCollection({
  ...props
}: React.ComponentProps<typeof Combobox.Collection>) {
  return (
    <Combobox.Collection
      data-slot="combobox-collection"
      {...props}
    />
  );
}

export {
  ComboboxRoot as Combobox,
  ComboboxInput,
  ComboboxTrigger,
  ComboboxIcon,
  ComboboxClear,
  ComboboxValue,
  ComboboxChips,
  ComboboxChip,
  ComboboxChipRemove,
  ComboboxPortal,
  ComboboxBackdrop,
  ComboboxPositioner,
  ComboboxPopup,
  ComboboxArrow,
  ComboboxStatus,
  ComboboxEmpty,
  ComboboxList,
  ComboboxRow,
  ComboboxItem,
  ComboboxItemIndicator,
  ComboboxSeparator,
  ComboboxGroup,
  ComboboxGroupLabel,
  ComboboxCollection,
};

Usage

WithClearButton

export const WithClearButton = () => {
  const id = `combobox-clear-${Math.random().toString(36).substr(2, 9)}`;
  return (
    <div className="space-y-1">
      <Label htmlFor={id}>Select a fruit</Label>
      <Combobox items={fruits}>
        <div className="flex items-center gap-0.5 border border-gray-6 rounded-sm px-2 py-1.5 focus-within:ring-2 focus-within:ring-gray-8">
          <ComboboxInput
            id={id}
            placeholder="Search fruits..."
            className="flex-1 outline-none bg-transparent"
          />
          <ComboboxClear
            className="p-0.5 hover:bg-accent-3 rounded-sm cursor-pointer"
            aria-label="Clear selection"
          >
            <Cross2Icon className="size-1" />
          </ComboboxClear>
        </div>
        <ComboboxPortal>
          <ComboboxPositioner sideOffset={4}>
            <ComboboxPopup className="w-40">
              <ComboboxEmpty>No fruits found.</ComboboxEmpty>
              <ComboboxList>
                {(item: string) => (
                  <ComboboxItem key={item} value={item}>
                    <ComboboxItemIndicator>
                      <CheckIcon className="size-1" />
                    </ComboboxItemIndicator>
                    <span>{item}</span>
                  </ComboboxItem>
                )}
              </ComboboxList>
            </ComboboxPopup>
          </ComboboxPositioner>
        </ComboboxPortal>
      </Combobox>
    </div>
  );
};

WithTriggerButton

export const WithTriggerButton = () => {
  const id = useId();
  return (
    <div className="space-y-1">
      <Label htmlFor={id}>Choose a fruit</Label>
      <Combobox items={fruits}>
        <div className="flex items-center gap-0.5 border border-gray-6 rounded-sm px-2 py-1.5 focus-within:ring-2 focus-within:ring-gray-8">
          <ComboboxInput
            id={id}
            placeholder="Select a fruit"
            className="flex-1 outline-none bg-transparent"
          />
          <ComboboxTrigger
            className="p-0.5 hover:bg-accent-3 rounded-sm cursor-pointer"
            aria-label="Open popup"
          >
            <ChevronDownIcon className="size-1" />
          </ComboboxTrigger>
        </div>
        <ComboboxPortal>
          <ComboboxPositioner sideOffset={4}>
            <ComboboxPopup className="w-40">
              <ComboboxEmpty>No fruits found.</ComboboxEmpty>
              <ComboboxList>
                {(item: string) => (
                  <ComboboxItem key={item} value={item}>
                    <ComboboxItemIndicator>
                      <CheckIcon className="size-1" />
                    </ComboboxItemIndicator>
                    <span>{item}</span>
                  </ComboboxItem>
                )}
              </ComboboxList>
            </ComboboxPopup>
          </ComboboxPositioner>
        </ComboboxPortal>
      </Combobox>
    </div>
  );
};

WithChips

export const WithChips = () => {
  const id = useId();
  return (
    <div className="space-y-1">
      <Label htmlFor={id}>Select multiple fruits</Label>
      <Combobox items={fruits} multiple>
        <ComboboxChips className="min-h-9 flex-wrap gap-1.5 border border-gray-6 rounded-sm px-2 py-1.5 focus-within:ring-2 focus-within:ring-gray-8">
          <ComboboxChip>
            <ComboboxValue />
            <ComboboxChipRemove>
              <Cross2Icon className="size-1" />
            </ComboboxChipRemove>
          </ComboboxChip>
          <ComboboxInput
            id={id}
            placeholder="Add fruits..."
            className="min-w-16 flex-1 outline-none bg-transparent"
          />
        </ComboboxChips>
        <ComboboxPortal>
          <ComboboxPositioner sideOffset={4}>
            <ComboboxPopup className="w-40">
              <ComboboxEmpty>No fruits found.</ComboboxEmpty>
              <ComboboxList>
                {(item: string) => (
                  <ComboboxItem key={item} value={item}>
                    <ComboboxItemIndicator>
                      <CheckIcon className="size-1" />
                    </ComboboxItemIndicator>
                    <span>{item}</span>
                  </ComboboxItem>
                )}
              </ComboboxList>
            </ComboboxPopup>
          </ComboboxPositioner>
        </ComboboxPortal>
      </Combobox>
    </div>
  );
};

WithGroups

export const WithGroups = () => {
  const id = useId();
  const regions = Array.from(new Set(countries.map((c) => c.region)));

  return (
    <div className="space-y-1">
      <Label htmlFor={id}>Select a country</Label>
      <Combobox items={countries} getItemValue={(item) => item.value}>
        <div className="flex items-center gap-0.5 border border-gray-6 rounded-sm px-2 py-1.5 focus-within:ring-2 focus-within:ring-gray-8">
          <ComboboxInput
            id={id}
            placeholder="Search countries..."
            className="flex-1 outline-none bg-transparent"
          />
          <div className="flex items-center gap-0.5">
            <ComboboxClear
              className="p-0.5 hover:bg-accent-3 rounded-sm cursor-pointer"
              aria-label="Clear selection"
            >
              <Cross2Icon className="size-1" />
            </ComboboxClear>
            <ComboboxTrigger
              className="p-0.5 hover:bg-accent-3 rounded-sm cursor-pointer"
              aria-label="Open popup"
            >
              <ChevronDownIcon className="size-1" />
            </ComboboxTrigger>
          </div>
        </div>
        <ComboboxPortal>
          <ComboboxPositioner sideOffset={4}>
            <ComboboxPopup className="w-40">
              <ComboboxEmpty>No countries found.</ComboboxEmpty>
              <ComboboxList>
                {regions.map((region) => (
                  <ComboboxGroup key={region}>
                    <ComboboxGroupLabel>{region}</ComboboxGroupLabel>
                    {countries
                      .filter((c) => c.region === region)
                      .map((country) => (
                        <ComboboxItem key={country.value} value={country.value}>
                          <ComboboxItemIndicator>
                            <CheckIcon className="size-1" />
                          </ComboboxItemIndicator>
                          <span>{country.label}</span>
                        </ComboboxItem>
                      ))}
                  </ComboboxGroup>
                ))}
              </ComboboxList>
            </ComboboxPopup>
          </ComboboxPositioner>
        </ComboboxPortal>
      </Combobox>
    </div>
  );
};

WithSeparator

export const WithSeparator = () => {
  const id = useId();
  const popularFruits = ["Apple", "Banana", "Orange", "Grape"];
  const otherFruits = fruits.filter((f) => !popularFruits.includes(f));

  return (
    <div className="space-y-1">
      <Label htmlFor={id}>Choose a fruit</Label>
      <Combobox items={fruits}>
        <div className="flex items-center gap-0.5 border border-gray-6 rounded-sm px-2 py-1.5 focus-within:ring-2 focus-within:ring-gray-8">
          <ComboboxInput
            id={id}
            placeholder="e.g. Apple"
            className="flex-1 outline-none bg-transparent"
          />
          <div className="flex items-center gap-0.5">
            <ComboboxClear
              className="p-0.5 hover:bg-accent-3 rounded-sm cursor-pointer"
              aria-label="Clear selection"
            >
              <Cross2Icon className="size-1" />
            </ComboboxClear>
            <ComboboxTrigger
              className="p-0.5 hover:bg-accent-3 rounded-sm cursor-pointer"
              aria-label="Open popup"
            >
              <ChevronDownIcon className="size-1" />
            </ComboboxTrigger>
          </div>
        </div>
        <ComboboxPortal>
          <ComboboxPositioner sideOffset={4}>
            <ComboboxPopup className="w-40">
              <ComboboxEmpty>No fruits found.</ComboboxEmpty>
              <ComboboxList>
                <ComboboxGroup>
                  <ComboboxGroupLabel>Popular</ComboboxGroupLabel>
                  {popularFruits.map((item) => (
                    <ComboboxItem key={item} value={item}>
                      <ComboboxItemIndicator>
                        <CheckIcon className="size-1" />
                      </ComboboxItemIndicator>
                      <span>{item}</span>
                    </ComboboxItem>
                  ))}
                </ComboboxGroup>
                <ComboboxSeparator />
                <ComboboxGroup>
                  <ComboboxGroupLabel>Others</ComboboxGroupLabel>
                  {otherFruits.map((item) => (
                    <ComboboxItem key={item} value={item}>
                      <ComboboxItemIndicator>
                        <CheckIcon className="size-1" />
                      </ComboboxItemIndicator>
                      <span>{item}</span>
                    </ComboboxItem>
                  ))}
                </ComboboxGroup>
              </ComboboxList>
            </ComboboxPopup>
          </ComboboxPositioner>
        </ComboboxPortal>
      </Combobox>
    </div>
  );
};

Controlled

export const Controlled = () => {
  const [value, setValue] = useState<string | null>(null);
  const id = useId();

  return (
    <div className="space-y-1">
      <div className="flex items-center gap-0.5">
        <span className="text-sm text-gray-11">Selected:</span>
        <span className="text-sm font-medium">{value || "None"}</span>
      </div>
      <Label htmlFor={id}>Choose a fruit</Label>
      <Combobox items={fruits} value={value} onValueChange={setValue}>
        <div className="flex items-center gap-0.5 border border-gray-6 rounded-sm px-2 py-1.5 focus-within:ring-2 focus-within:ring-gray-8">
          <ComboboxInput
            id={id}
            placeholder="e.g. Apple"
            className="flex-1 outline-none bg-transparent"
          />
          <div className="flex items-center gap-0.5">
            <ComboboxClear
              className="p-0.5 hover:bg-accent-3 rounded-sm cursor-pointer"
              aria-label="Clear selection"
            >
              <Cross2Icon className="size-1" />
            </ComboboxClear>
            <ComboboxTrigger
              className="p-0.5 hover:bg-accent-3 rounded-sm cursor-pointer"
              aria-label="Open popup"
            >
              <ChevronDownIcon className="size-1" />
            </ComboboxTrigger>
          </div>
        </div>
        <ComboboxPortal>
          <ComboboxPositioner sideOffset={4}>
            <ComboboxPopup className="w-40">
              <ComboboxEmpty>No fruits found.</ComboboxEmpty>
              <ComboboxList>
                {(item: string) => (
                  <ComboboxItem key={item} value={item}>
                    <ComboboxItemIndicator>
                      <CheckIcon className="size-1" />
                    </ComboboxItemIndicator>
                    <span>{item}</span>
                  </ComboboxItem>
                )}
              </ComboboxList>
            </ComboboxPopup>
          </ComboboxPositioner>
        </ComboboxPortal>
      </Combobox>
      <Button
        variant="link"
        size="sm"
        onClick={() => setValue(null)}
        className="mt-0.5"
      >
        Clear Selection
      </Button>
    </div>
  );
};

Disabled

export const Disabled = () => {
  const id = useId();
  return (
    <div className="space-y-1">
      <Label htmlFor={id}>Choose a fruit (disabled)</Label>
      <Combobox items={fruits} disabled>
        <div className="flex items-center gap-0.5 border border-gray-6 rounded-sm px-2 py-1.5 opacity-50 cursor-not-allowed">
          <ComboboxInput
            id={id}
            placeholder="e.g. Apple"
            className="flex-1 outline-none bg-transparent"
            disabled
          />
          <div className="flex items-center gap-0.5">
            <ComboboxClear
              className="p-0.5 hover:bg-accent-3 rounded-sm cursor-pointer"
              aria-label="Clear selection"
              disabled
            >
              <Cross2Icon className="size-1" />
            </ComboboxClear>
            <ComboboxTrigger
              className="p-0.5 hover:bg-accent-3 rounded-sm cursor-pointer"
              aria-label="Open popup"
              disabled
            >
              <ChevronDownIcon className="size-1" />
            </ComboboxTrigger>
          </div>
        </div>
        <ComboboxPortal>
          <ComboboxPositioner sideOffset={4}>
            <ComboboxPopup className="w-40">
              <ComboboxEmpty>No fruits found.</ComboboxEmpty>
              <ComboboxList>
                {(item: string) => (
                  <ComboboxItem key={item} value={item}>
                    <ComboboxItemIndicator>
                      <CheckIcon className="size-1" />
                    </ComboboxItemIndicator>
                    <span>{item}</span>
                  </ComboboxItem>
                )}
              </ComboboxList>
            </ComboboxPopup>
          </ComboboxPositioner>
        </ComboboxPortal>
      </Combobox>
    </div>
  );
};

Positioning

export const Positioning = () => {
  const id = useId();

  return (
    <div className="grid grid-cols-2 gap-4">
      {(["top", "right", "bottom", "left"] as const).map((side) => (
        <div key={side} className="space-y-1">
          <Label htmlFor={id} className="capitalize">
            {side}
          </Label>
          <Combobox items={fruits}>
            <div className="flex items-center gap-0.5 border border-gray-6 rounded-sm px-2 py-1.5 focus-within:ring-2 focus-within:ring-gray-8">
              <ComboboxInput
                id={id}
                placeholder="e.g. Apple"
                className="flex-1 outline-none bg-transparent"
              />
              <div className="flex items-center gap-0.5">
                <ComboboxClear
                  className="p-0.5 hover:bg-accent-3 rounded-sm cursor-pointer"
                  aria-label="Clear selection"
                >
                  <Cross2Icon className="size-1" />
                </ComboboxClear>
                <ComboboxTrigger
                  className="p-0.5 hover:bg-accent-3 rounded-sm cursor-pointer"
                  aria-label="Open popup"
                >
                  <ChevronDownIcon className="size-1" />
                </ComboboxTrigger>
              </div>
            </div>
            <ComboboxPortal>
              <ComboboxPositioner side={side} sideOffset={4}>
                <ComboboxPopup className="w-40">
                  <ComboboxEmpty>No fruits found.</ComboboxEmpty>
                  <ComboboxList>
                    {(item: string) => (
                      <ComboboxItem key={item} value={item}>
                        <ComboboxItemIndicator>
                          <CheckIcon className="size-1" />
                        </ComboboxItemIndicator>
                        <span>{item}</span>
                      </ComboboxItem>
                    )}
                  </ComboboxList>
                </ComboboxPopup>
              </ComboboxPositioner>
            </ComboboxPortal>
          </Combobox>
        </div>
      ))}
    </div>
  );
};