Fiber UI LogoFiberUI

ComboBox

A combo box combines a text input with a listbox, allowing users to filter a list of options to items matching a query. Supports sections, custom items, async loading, and form integration.

The ComboBox component combines a text input with a dropdown list, allowing users to type to filter options. Built on React Aria's ComboBox primitive with full keyboard navigation, accessibility, and animation support.

Basic Usage

A simple combo box with static items. Type to filter the list of options.

"use client";
import { ComboBox, ComboBoxItem } from "@repo/ui/components/combo-box";

/* BASIC EXAMPLE */
export const Example1 = () => {
    return (
        <ComboBox label="Favorite fruit" placeholder="Select a fruit">
            <ComboBoxItem>Apple</ComboBoxItem>
            <ComboBoxItem>Banana</ComboBoxItem>
            <ComboBoxItem>Orange</ComboBoxItem>
            <ComboBoxItem>Strawberry</ComboBoxItem>
            <ComboBoxItem>Mango</ComboBoxItem>
            <ComboBoxItem>Grape</ComboBoxItem>
        </ComboBox>
    );
};

Controlled Selection

Use selectedKey and onSelectionChange to control the selected item.

Selected: cat

"use client";
import { useState } from "react";
import type { Key } from "react-aria-components";
import { ComboBox, ComboBoxItem } from "@repo/ui/components/combo-box";

/* CONTROLLED SELECTION EXAMPLE */
export const Example2 = () => {
    const [animal, setAnimal] = useState<Key | null>("cat");

    return (
        <div className="flex flex-col gap-2">
            <ComboBox
                label="Favorite animal"
                placeholder="Select an animal"
                selectedKey={animal}
                onSelectionChange={setAnimal}
            >
                <ComboBoxItem id="cat">Cat</ComboBoxItem>
                <ComboBoxItem id="dog">Dog</ComboBoxItem>
                <ComboBoxItem id="koala">Koala</ComboBoxItem>
                <ComboBoxItem id="kangaroo">Kangaroo</ComboBoxItem>
                <ComboBoxItem id="panda">Panda</ComboBoxItem>
            </ComboBox>
            <p className="text-muted-foreground text-sm">
                Selected: {animal ?? "none"}
            </p>
        </div>
    );
};

Sections

Use ComboBoxSection and ComboBoxHeader with the Collection Components API for grouped items.

"use client";
import {
    ComboBox,
    ComboBoxItem,
    ComboBoxSection,
    ComboBoxHeader,
} from "@repo/ui/components/combo-box";
import { Collection } from "react-aria-components";

/* GROUPED/SECTIONED EXAMPLE */
const options = [
    {
        name: "Fruit",
        children: [
            { id: "apple", name: "Apple" },
            { id: "banana", name: "Banana" },
            { id: "orange", name: "Orange" },
            { id: "grapes", name: "Grapes" },
        ],
    },
    {
        name: "Vegetable",
        children: [
            { id: "carrot", name: "Carrot" },
            { id: "broccoli", name: "Broccoli" },
            { id: "spinach", name: "Spinach" },
            { id: "potato", name: "Potato" },
        ],
    },
];

export const Example3 = () => {
    return (
        <ComboBox
            label="Preferred food"
            placeholder="Select a food"
            defaultItems={options}
        >
            {(section) => (
                <ComboBoxSection id={section.name}>
                    <ComboBoxHeader>{section.name}</ComboBoxHeader>
                    <Collection items={section.children}>
                        {(item) => (
                            <ComboBoxItem id={item.id}>
                                {item.name}
                            </ComboBoxItem>
                        )}
                    </Collection>
                </ComboBoxSection>
            )}
        </ComboBox>
    );
};

Custom Items

Items can render any content including icons and descriptions.

"use client";
import {
    GlobeIcon,
    CodeIcon,
    PaletteIcon,
    ServerIcon,
    SmartphoneIcon,
} from "lucide-react";
import { ComboBox, ComboBoxItem } from "@repo/ui/components/combo-box";

/* CUSTOM ITEMS WITH ICONS */
export const Example4 = () => {
    return (
        <ComboBox label="Technology" placeholder="Select a technology">
            <ComboBoxItem id="web" textValue="Web Development">
                <GlobeIcon />
                <div className="flex flex-col">
                    <span className="font-medium">Web Development</span>
                    <span className="text-muted-foreground text-xs">
                        HTML, CSS, JavaScript
                    </span>
                </div>
            </ComboBoxItem>
            <ComboBoxItem id="backend" textValue="Backend">
                <CodeIcon />
                <div className="flex flex-col">
                    <span className="font-medium">Backend</span>
                    <span className="text-muted-foreground text-xs">
                        Node.js, Python, Go
                    </span>
                </div>
            </ComboBoxItem>
            <ComboBoxItem id="design" textValue="Design">
                <PaletteIcon />
                <div className="flex flex-col">
                    <span className="font-medium">Design</span>
                    <span className="text-muted-foreground text-xs">
                        Figma, Sketch, Adobe XD
                    </span>
                </div>
            </ComboBoxItem>
            <ComboBoxItem id="devops" textValue="DevOps">
                <ServerIcon />
                <div className="flex flex-col">
                    <span className="font-medium">DevOps</span>
                    <span className="text-muted-foreground text-xs">
                        Docker, Kubernetes, CI/CD
                    </span>
                </div>
            </ComboBoxItem>
            <ComboBoxItem id="mobile" textValue="Mobile">
                <SmartphoneIcon />
                <div className="flex flex-col">
                    <span className="font-medium">Mobile</span>
                    <span className="text-muted-foreground text-xs">
                        React Native, Flutter, Swift
                    </span>
                </div>
            </ComboBoxItem>
        </ComboBox>
    );
};

Async Loading

Use items, inputValue, and onInputChange for async filtering with dynamic collections.

"use client";
import { useState } from "react";
import { LoaderIcon } from "lucide-react";
import { ComboBox, ComboBoxItem } from "@repo/ui/components/combo-box";

/* ASYNC LOADING EXAMPLE */
const allItems = [
    { id: "react", name: "React" },
    { id: "angular", name: "Angular" },
    { id: "vue", name: "Vue" },
    { id: "svelte", name: "Svelte" },
    { id: "solid", name: "Solid" },
    { id: "next", name: "Next.js" },
    { id: "nuxt", name: "Nuxt" },
    { id: "remix", name: "Remix" },
    { id: "astro", name: "Astro" },
];

export const Example5 = () => {
    const [inputValue, setInputValue] = useState("");
    const [isLoading, setIsLoading] = useState(false);
    const [items, setItems] = useState(allItems);

    const onInputChange = (value: string) => {
        setInputValue(value);
        setIsLoading(true);

        // Simulate async search
        setTimeout(() => {
            setItems(
                allItems.filter((item) =>
                    item.name.toLowerCase().includes(value.toLowerCase()),
                ),
            );
            setIsLoading(false);
        }, 500);
    };

    return (
        <div className="flex flex-col gap-2">
            <ComboBox
                label="Framework"
                placeholder="Search frameworks..."
                items={items}
                inputValue={inputValue}
                onInputChange={onInputChange}
                allowsEmptyCollection
            >
                {(item) => (
                    <ComboBoxItem id={item.id}>{item.name}</ComboBoxItem>
                )}
            </ComboBox>
            {isLoading && (
                <div className="text-muted-foreground flex items-center gap-2 text-sm">
                    <LoaderIcon className="size-3 animate-spin" />
                    Loading...
                </div>
            )}
        </div>
    );
};

Disabled States

Use isDisabled on the ComboBox or individual items.

"use client";
import { ComboBox, ComboBoxItem } from "@repo/ui/components/combo-box";

/* DISABLED STATES EXAMPLE */
export const Example6 = () => {
    return (
        <div className="flex flex-col gap-4">
            <ComboBox
                label="Disabled combo box"
                placeholder="Select an option"
                isDisabled
            >
                <ComboBoxItem>Option 1</ComboBoxItem>
                <ComboBoxItem>Option 2</ComboBoxItem>
                <ComboBoxItem>Option 3</ComboBoxItem>
            </ComboBox>

            <ComboBox label="With disabled items" placeholder="Select a color">
                <ComboBoxItem id="red">Red</ComboBoxItem>
                <ComboBoxItem id="green" isDisabled>
                    Green (disabled)
                </ComboBoxItem>
                <ComboBoxItem id="blue">Blue</ComboBoxItem>
                <ComboBoxItem id="yellow" isDisabled>
                    Yellow (disabled)
                </ComboBoxItem>
            </ComboBox>
        </div>
    );
};

Form Integration

Use name, isRequired, and description for form usage with built-in validation support.

Please select your country of residence.
"use client";
import { ComboBox, ComboBoxItem } from "@repo/ui/components/combo-box";

/* FORM INTEGRATION WITH DESCRIPTION */
export const Example7 = () => {
    return (
        <ComboBox
            label="Country"
            placeholder="e.g. United States"
            name="country"
            isRequired
            description="Please select your country of residence."
        >
            <ComboBoxItem id="us">United States</ComboBoxItem>
            <ComboBoxItem id="uk">United Kingdom</ComboBoxItem>
            <ComboBoxItem id="ca">Canada</ComboBoxItem>
            <ComboBoxItem id="au">Australia</ComboBoxItem>
            <ComboBoxItem id="de">Germany</ComboBoxItem>
            <ComboBoxItem id="jp">Japan</ComboBoxItem>
        </ComboBox>
    );
};

Component Code

"use client";

import * as React from "react";
import { CheckIcon, ChevronDownIcon } from "lucide-react";
import {
    ComboBox as AriaComboBox,
    ComboBoxProps as AriaComboBoxProps,
    Input as AriaInput,
    Button as AriaButton,
    Popover as AriaPopover,
    ListBox as AriaListBox,
    ListBoxItem as AriaListBoxItem,
    ListBoxSection as AriaListBoxSection,
    Header as AriaHeader,
    FieldError as AriaFieldError,
    Text as AriaText,
    composeRenderProps,
    type ListBoxItemProps as AriaListBoxItemProps,
    type SectionProps as AriaSectionProps,
    type ValidationResult,
} from "react-aria-components";
import { cn, tv } from "tailwind-variants";
import { Label } from "@repo/ui/components/label";

/* -----------------------------------------------------------------------------
 * ComboBox (Root)
 * ---------------------------------------------------------------------------*/

const comboBoxStyles = tv({
    base: "group flex flex-col gap-1.5",
});

const fieldGroupStyles = tv({
    base: [
        "border-input [&_svg:not([class*='text-'])]:text-muted-foreground",
        "focus-within:border-ring focus-within:ring-ring/50",
        "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
        "dark:bg-input/30 dark:hover:bg-input/50",
        "shadow-xs flex h-9 items-center rounded-md border bg-transparent text-sm",
        "outline-none transition-[color,box-shadow] focus-within:ring-[3px]",
        "group-data-[disabled]:cursor-not-allowed group-data-[disabled]:opacity-50",
    ],
});

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

export function ComboBox<T extends object>({
    label,
    description,
    errorMessage,
    children,
    items,
    placeholder,
    className,
    ...props
}: ComboBoxProps<T>) {
    return (
        <AriaComboBox
            data-slot="combo-box"
            className={cn(comboBoxStyles(), className)}
            {...props}
        >
            {label && <Label>{label}</Label>}
            <div className={fieldGroupStyles()}>
                <AriaInput
                    data-slot="combo-box-input"
                    placeholder={placeholder}
                    className="placeholder:text-muted-foreground flex-1 bg-transparent px-3 py-2 outline-none"
                />
                <AriaButton
                    data-slot="combo-box-button"
                    className="flex items-center justify-center pr-2 outline-none"
                >
                    <ChevronDownIcon className="size-4 opacity-50" />
                </AriaButton>
            </div>
            {description && (
                <AriaText
                    slot="description"
                    className="text-muted-foreground text-xs"
                >
                    {description}
                </AriaText>
            )}
            <AriaFieldError className="text-destructive text-xs">
                {errorMessage}
            </AriaFieldError>
            <AriaPopover
                data-slot="combo-box-popover"
                className={cn(
                    "bg-popover text-popover-foreground",
                    "data-entering:animate-in data-exiting:animate-out",
                    "data-exiting:fade-out-0 data-entering:fade-in-0",
                    "data-exiting:zoom-out-95 data-entering:zoom-in-95",
                    "data-[placement=bottom]:slide-in-from-top-2 data-[placement=left]:slide-in-from-right-2",
                    "data-[placement=right]:slide-in-from-left-2 data-[placement=top]:slide-in-from-bottom-2",
                    "w-(--trigger-width) relative z-50 overflow-hidden rounded-md border shadow-md",
                )}
            >
                <AriaListBox
                    data-slot="combo-box-listbox"
                    items={items}
                    className="max-h-[300px] overflow-y-auto p-1 outline-none"
                >
                    {children}
                </AriaListBox>
            </AriaPopover>
        </AriaComboBox>
    );
}

/* -----------------------------------------------------------------------------
 * ComboBoxItem
 * ---------------------------------------------------------------------------*/

interface ComboBoxItemProps extends Omit<AriaListBoxItemProps, "value"> {
    value?: string;
}

export function ComboBoxItem({
    className,
    children,
    value,
    id,
    ...props
}: ComboBoxItemProps) {
    return (
        <AriaListBoxItem
            data-slot="combo-box-item"
            className={cn(
                "focus:bg-accent focus:text-accent-foreground",
                "[&_svg:not([class*='text-'])]:text-muted-foreground",
                "outline-hidden relative flex cursor-default select-none items-center gap-2 rounded-sm py-1.5 pl-2 pr-8 text-sm",
                "data-disabled:pointer-events-none data-disabled:opacity-50",
                "[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
                className,
            )}
            {...props}
            id={id == undefined ? value : id}
        >
            {composeRenderProps(children, (children, { isSelected }) => (
                <>
                    <span className="flex flex-1 items-center gap-2 truncate">
                        {children}
                    </span>
                    {isSelected && (
                        <span
                            data-slot="combo-box-item-indicator"
                            className="absolute right-2 flex size-3.5 items-center justify-center"
                        >
                            <CheckIcon className="size-4" />
                        </span>
                    )}
                </>
            ))}
        </AriaListBoxItem>
    );
}

/* -----------------------------------------------------------------------------
 * ComboBoxSection
 * ---------------------------------------------------------------------------*/

interface ComboBoxSectionProps<T extends object> extends AriaSectionProps<T> {}

export function ComboBoxSection<T extends object>({
    className,
    ...props
}: ComboBoxSectionProps<T>) {
    return (
        <AriaListBoxSection
            data-slot="combo-box-section"
            className={cn("py-1", className)}
            {...props}
        />
    );
}

/* -----------------------------------------------------------------------------
 * ComboBoxHeader
 * ---------------------------------------------------------------------------*/

interface ComboBoxHeaderProps extends React.ComponentProps<typeof AriaHeader> {}

export function ComboBoxHeader({ className, ...props }: ComboBoxHeaderProps) {
    return (
        <AriaHeader
            data-slot="combo-box-header"
            className={cn(
                "text-muted-foreground px-2 py-1.5 text-xs font-medium",
                className,
            )}
            {...props}
        />
    );
}