Fiber UI LogoFiberUI

ListBox

A list of interactive items with keyboard navigation and selection support. Supports single and multiple selection, sections, disabled items, and custom empty states.

The ListBox component displays a list of options and allows a user to select one or more of them. Built on React Aria's ListBox primitive with full keyboard navigation and accessibility.

Basic Usage

A simple list box with single selection. Use arrow keys to navigate and Enter or Space to select.

Aardvark
Cat
Dog
Kangaroo
Panda
Snake
"use client";
import { ListBox, ListBoxItem } from "@repo/ui/components/list-box";

/* BASIC USAGE */
export const Example1 = () => {
    return (
        <ListBox aria-label="Favorite animal" selectionMode="single">
            <ListBoxItem>Aardvark</ListBoxItem>
            <ListBoxItem>Cat</ListBoxItem>
            <ListBoxItem>Dog</ListBoxItem>
            <ListBoxItem>Kangaroo</ListBoxItem>
            <ListBoxItem>Panda</ListBoxItem>
            <ListBoxItem>Snake</ListBoxItem>
        </ListBox>
    );
};

Controlled Single Selection

Use selectedKeys and onSelectionChange to control the selected item programmatically.

Aardvark
Cat
Dog
Kangaroo
Panda

Selected: cat

"use client";
import { useState } from "react";
import type { Selection } from "react-aria-components";
import { ListBox, ListBoxItem } from "@repo/ui/components/list-box";

/* CONTROLLED SINGLE SELECTION */
export const Example2 = () => {
    const [selected, setSelected] = useState<Selection>(new Set(["cat"]));

    return (
        <div className="flex flex-col gap-2">
            <ListBox
                aria-label="Favorite animal"
                selectionMode="single"
                selectedKeys={selected}
                onSelectionChange={setSelected}
            >
                <ListBoxItem id="aardvark">Aardvark</ListBoxItem>
                <ListBoxItem id="cat">Cat</ListBoxItem>
                <ListBoxItem id="dog">Dog</ListBoxItem>
                <ListBoxItem id="kangaroo">Kangaroo</ListBoxItem>
                <ListBoxItem id="panda">Panda</ListBoxItem>
            </ListBox>
            <p className="text-muted-foreground text-sm">
                Selected:{" "}
                {selected === "all"
                    ? "all"
                    : [...selected].join(", ") || "none"}
            </p>
        </div>
    );
};

Multiple Selection

Set selectionMode="multiple" to allow selecting multiple items. Click to toggle, or hold Shift to select a range.

Lettuce
Tomato
Cheese
Tuna Salad
Egg Salad
Ham

Selected: cheese, ham

"use client";
import { useState } from "react";
import type { Selection } from "react-aria-components";
import { ListBox, ListBoxItem } from "@repo/ui/components/list-box";

/* MULTIPLE SELECTION */
export const Example3 = () => {
    const [selected, setSelected] = useState<Selection>(
        new Set(["cheese", "ham"]),
    );

    return (
        <div className="flex flex-col gap-2">
            <ListBox
                aria-label="Sandwich contents"
                selectionMode="multiple"
                selectedKeys={selected}
                onSelectionChange={setSelected}
            >
                <ListBoxItem id="lettuce">Lettuce</ListBoxItem>
                <ListBoxItem id="tomato">Tomato</ListBoxItem>
                <ListBoxItem id="cheese">Cheese</ListBoxItem>
                <ListBoxItem id="tuna">Tuna Salad</ListBoxItem>
                <ListBoxItem id="egg">Egg Salad</ListBoxItem>
                <ListBoxItem id="ham">Ham</ListBoxItem>
            </ListBox>
            <p className="text-muted-foreground text-sm">
                Selected:{" "}
                {selected === "all"
                    ? "all"
                    : [...selected].join(", ") || "none"}
            </p>
        </div>
    );
};

Sections

Use DropdownSection with a title to group items into labeled categories with sticky headers.

Lettuce
Tomato
Onion
Ham
Tuna
Tofu
Mayonnaise
Mustard
Ranch
"use client";
import {
    ListBox,
    ListBoxItem,
    DropdownSection,
} from "@repo/ui/components/list-box";

/* SECTIONS WITH HEADERS */
export const Example4 = () => {
    return (
        <ListBox aria-label="Sandwich contents" selectionMode="multiple">
            <DropdownSection title="Veggies" items={undefined}>
                <ListBoxItem id="lettuce">Lettuce</ListBoxItem>
                <ListBoxItem id="tomato">Tomato</ListBoxItem>
                <ListBoxItem id="onion">Onion</ListBoxItem>
            </DropdownSection>
            <DropdownSection title="Protein" items={undefined}>
                <ListBoxItem id="ham">Ham</ListBoxItem>
                <ListBoxItem id="tuna">Tuna</ListBoxItem>
                <ListBoxItem id="tofu">Tofu</ListBoxItem>
            </DropdownSection>
            <DropdownSection title="Condiments" items={undefined}>
                <ListBoxItem id="mayo">Mayonnaise</ListBoxItem>
                <ListBoxItem id="mustard">Mustard</ListBoxItem>
                <ListBoxItem id="ranch">Ranch</ListBoxItem>
            </DropdownSection>
        </ListBox>
    );
};

Disabled Items

Use the disabledKeys prop to disable specific items. Disabled items cannot be selected or focused.

Read
Write
Admin (restricted)
Delete (restricted)
Comment
"use client";
import { ListBox, ListBoxItem } from "@repo/ui/components/list-box";

/* DISABLED ITEMS */
export const Example5 = () => {
    return (
        <ListBox
            aria-label="Permissions"
            selectionMode="single"
            disabledKeys={["admin", "delete"]}
        >
            <ListBoxItem id="read">Read</ListBoxItem>
            <ListBoxItem id="write">Write</ListBoxItem>
            <ListBoxItem id="admin">Admin (restricted)</ListBoxItem>
            <ListBoxItem id="delete">Delete (restricted)</ListBoxItem>
            <ListBoxItem id="comment">Comment</ListBoxItem>
        </ListBox>
    );
};

Empty State

Use renderEmptyState to display a custom message when the list has no items.

No results found.
"use client";
import { ListBox } from "@repo/ui/components/list-box";

/* EMPTY STATE */
export const Example6 = () => {
    return (
        <ListBox
            aria-label="Search results"
            selectionMode="single"
            renderEmptyState={() => (
                <div className="text-muted-foreground flex items-center justify-center py-6 text-sm italic">
                    No results found.
                </div>
            )}
        >
            {[]}
        </ListBox>
    );
};

Component Code

"use client";

import { Check } from "lucide-react";
import React from "react";
import {
    ListBox as AriaListBox,
    ListBoxItem as AriaListBoxItem,
    ListBoxProps as AriaListBoxProps,
    Collection,
    Header,
    ListBoxItemProps,
    ListBoxSection,
    SectionProps,
    composeRenderProps,
} from "react-aria-components";
import { cn, tv } from "tailwind-variants";
import { focusRing } from "@repo/ui/lib/utils";

interface ListBoxProps<T>
    extends Omit<AriaListBoxProps<T>, "layout" | "orientation"> {}

export function ListBox<T extends object>({
    children,
    ...props
}: ListBoxProps<T>) {
    return (
        <AriaListBox
            data-slot="list-box"
            {...props}
            className={cn(
                props.className,
                "border-border bg-popover w-[200px] rounded-lg border p-1 font-sans outline-0",
            )}
        >
            {children}
        </AriaListBox>
    );
}

export const itemStyles = tv({
    extend: focusRing,
    base: "group relative flex cursor-default select-none items-center gap-8 rounded-md px-2.5 py-1.5 text-sm will-change-transform forced-color-adjust-none",
    variants: {
        isSelected: {
            false: "text-popover-foreground hover:bg-accent pressed:bg-accent -outline-offset-2",
            true: "bg-primary text-primary-foreground -outline-offset-4 outline-white dark:outline-white forced-colors:bg-[Highlight] forced-colors:text-[HighlightText] forced-colors:outline-[HighlightText] [&+[data-selected]]:rounded-t-none [&:has(+[data-selected])]:rounded-b-none",
        },
        isDisabled: {
            true: "text-muted-foreground opacity-50 forced-colors:text-[GrayText]",
        },
    },
});

export function ListBoxItem(props: ListBoxItemProps) {
    let textValue =
        props.textValue ||
        (typeof props.children === "string" ? props.children : undefined);
    return (
        <AriaListBoxItem
            data-slot="list-box-item"
            {...props}
            textValue={textValue}
            className={itemStyles}
        >
            {composeRenderProps(props.children, (children) => (
                <>
                    {children}
                    <div className="bg-primary-foreground/20 absolute bottom-0 left-4 right-4 hidden h-px forced-colors:bg-[HighlightText] [.group[data-selected]:has(+[data-selected])_&]:block" />
                </>
            ))}
        </AriaListBoxItem>
    );
}

export const dropdownItemStyles = tv({
    base: "selected:pr-1 group flex cursor-default select-none items-center gap-4 rounded-lg py-2 pl-3 pr-3 text-sm no-underline outline outline-0 forced-color-adjust-none [-webkit-tap-highlight-color:transparent] [&[href]]:cursor-pointer",
    variants: {
        isDisabled: {
            false: "text-popover-foreground",
            true: "text-muted-foreground opacity-50 forced-colors:text-[GrayText]",
        },
        isPressed: {
            true: "bg-accent",
        },
        isFocused: {
            true: "bg-primary text-primary-foreground forced-colors:bg-[Highlight] forced-colors:text-[HighlightText]",
        },
    },
    compoundVariants: [
        {
            isFocused: false,
            isOpen: true,
            className: "bg-accent",
        },
    ],
});

export function DropdownItem(props: ListBoxItemProps) {
    let textValue =
        props.textValue ||
        (typeof props.children === "string" ? props.children : undefined);
    return (
        <AriaListBoxItem
            data-slot="dropdown-item"
            {...props}
            textValue={textValue}
            className={dropdownItemStyles}
        >
            {composeRenderProps(props.children, (children, { isSelected }) => (
                <>
                    <span className="group-selected:font-semibold flex flex-1 items-center gap-2 truncate font-normal">
                        {children}
                    </span>
                    <span className="flex w-5 items-center">
                        {isSelected && <Check className="h-4 w-4" />}
                    </span>
                </>
            ))}
        </AriaListBoxItem>
    );
}

export interface DropdownSectionProps<T> extends SectionProps<T> {
    title?: string;
    items?: any;
}

export function DropdownSection<T extends object>(
    props: DropdownSectionProps<T>,
) {
    return (
        <ListBoxSection
            data-slot="dropdown-section"
            className="after:block after:h-[5px] after:content-[''] first:-mt-[5px] last:after:hidden"
        >
            <Header className="text-muted-foreground border-y-border bg-muted/60 supports-[-moz-appearance:none]:bg-muted sticky -top-[5px] z-10 -mx-1 -mt-px truncate border-y px-4 py-1 text-sm font-semibold backdrop-blur-md [&+*]:mt-1">
                {props.title}
            </Header>
            <Collection items={props.items}>{props.children}</Collection>
        </ListBoxSection>
    );
}