ComboBox

A combo box combines a text input with a listbox, allowing users to filter a list of options to items matching a query.


Installation

Install via VScode or copy and paste the code below into a new file, preferably in a folder at components/base.

BaseLayer

import type {
  ComboBoxProps as AriaComboBoxProps,
  ListBoxItemProps,
} from "react-aria-components";

import {
  ComboBox as AriaComboBox,
  Button,
  Input,
  ListBoxItem,
  Label,
  ListBox,
  Popover,
  Text,
} from "react-aria-components";

import { ChevronDown } from "lucide-react";
import { tv } from "tailwind-variants";

const combobox = tv({
  slots: {
    input:
      "m-0 w-64 rounded-md border border-border bg-surface p-2 align-middle text-fg outline-none ring-fg focus:border-transparent focus:ring-2",
    root: "max-h-inherit overflow-auto p-1 outline-none",
    item: "relative m-1 flex cursor-default flex-col rounded-md p-2 outline-none aria-selected:bg-secondary aria-selected:text-secondary-fg data-[focused]:bg-surface-2 data-[focused]:aria-selected:bg-secondary ",
    popover:
      "w-64 rounded-xl border border-border bg-surface p-2 text-fg shadow-xl outline-none",
    button:
      "absolute right-2 flex appearance-none items-center justify-center rounded-md border-0 outline-none data-[hovered]:bg-surface-2",
  },
});

const { input, button, item, popover, root } = combobox();

interface ComboBoxProps<T extends object>
  extends Omit<AriaComboBoxProps<T>, "children"> {
  className?: string;
  label?: string;
  description?: string | null;
  errorMessage?: string | null;
  children: React.ReactNode | ((item: T) => React.ReactNode);
}

const ComboBox = <T extends object>({
  label,
  className,
  description,
  errorMessage,
  children,
  ...props
}: ComboBoxProps<T>) => (
  <AriaComboBox className={root({ className })} {...props}>
    <Label className="text-fg">{label}</Label>
    <div className="relative flex w-fit items-center rounded-2xl bg-surface">
      <Input className={input()} />
      <Button className={button()}>
        <ChevronDown className="text-fg" />
      </Button>
    </div>
    {description && <Text slot="description">{description}</Text>}
    {errorMessage && <Text slot="errorMessage">{errorMessage}</Text>}
    <Popover className={popover()}>
      <ListBox>{children}</ListBox>
    </Popover>
  </AriaComboBox>
);

ComboBox.displayName = "ComboBox";

const ComboBoxItem = (props: ListBoxItemProps) => (
  <ListBoxItem {...props} className={item()} />
);

ComboBoxItem.displayName = "ComboBoxItem";

export { ComboBox, ComboBoxItem };

Examples

Default