dotUI
dotUI
beta
  1. Components
  2. Menus and selection
  3. ListBox

ListBox

A list of options that can allow selection of one or more.

<ListBox>
  <Item>Next.js</Item>
  <Item>Remix</Item>
  <Item>Astro</Item>
  <Item>Gatsby</Item>
</ListBox>

Installation

Install the following dependencies:

npm install react-aria-components

Copy and paste the following code into your project.

"use client";

import React from "react";
import {
  composeRenderProps,
  ListBox as AriaListBox,
  ListBoxItem as AriaListBoxItem,
  Collection as AriaCollection,
  type ListBoxProps as AriaListBoxProps,
  type ListBoxItemProps as AriaListBoxItemProps,
  ListStateContext,
} from "react-aria-components";
import { tv, type VariantProps } from "tailwind-variants";
import { CheckIcon, LoaderIcon } from "@/lib/icons";
import { focusRing } from "@/lib/utils/styles";
import { Text } from "./text";

const listBoxStyles = tv({
  base: [
    focusRing(),
    "flex flex-col p-1 overflow-auto outline-none orientation-horizontal:flex-row orientation-horizontal:w-auto layout-grid:grid layout-grid:grid-cols-2 layout-grid:w-auto empty:justify-center empty:items-center empty:italic empty:min-h-24 empty:text-fg-muted empty:text-sm",
    "[&_.separator]:-mx-1 [&_.separator]:my-1 [&_.separator]:w-auto",
  ],
  variants: {
    standalone: {
      true: "border bg-bg w-48 rounded-md max-h-60 overflow-y-auto",
      false: "rounded-[inherit] max-h-[inherit]",
    },
  },
});

const listBoxItemStyles = tv({
  base: [
    "flex cursor-pointer items-center rounded-sm px-3 py-1.5 text-sm outline-none transition-colors disabled:pointer-default hover:bg-bg-inverse/10 focus:bg-bg-inverse/10 pressed:bg-bg-inverse/15 disabled:text-fg-disabled disabled:cursor-default",
    "selection-single:pl-0 selection-multiple:pl-0",
    "[&_svg]:size-4",
  ],
  variants: {
    variant: {
      default: "text-fg",
      success: "text-fg-success",
      warning: "text-fg-warning",
      accent: "text-fg-accent",
      danger: "text-fg-danger",
    },
  },
  defaultVariants: {
    variant: "default",
  },
});

interface ListBoxProps<T> extends AriaListBoxProps<T> {
  isLoading?: boolean;
}
const ListBox = <T extends object>({ children, isLoading, ...props }: ListBoxProps<T>) => {
  const state = React.useContext(ListStateContext);
  const standalone = !state;
  return (
    <AriaListBox
      {...props}
      className={composeRenderProps(props.className, (className) =>
        listBoxStyles({ standalone, className })
      )}
    >
      <AriaCollection items={props.items}>{children}</AriaCollection>
      {isLoading && (
        <AriaListBoxItem className="flex items-center justify-center py-1.5">
          <LoaderIcon aria-label="Loading more..." className="size-5 animate-spin text-fg-muted" />
        </AriaListBoxItem>
      )}
    </AriaListBox>
  );
};

interface ItemProps<T> extends AriaListBoxItemProps<T>, VariantProps<typeof listBoxItemStyles> {
  label?: string;
  description?: string;
  prefix?: React.ReactNode;
  suffix?: React.ReactNode;
}
const Item = <T extends object>({
  variant,
  label,
  description,
  prefix,
  suffix,
  ...props
}: ItemProps<T>) => {
  const textValue =
    props.textValue || (typeof props.children === "string" ? props.children : undefined);
  return (
    <AriaListBoxItem
      {...props}
      textValue={textValue}
      className={composeRenderProps(props.className, (className) =>
        listBoxItemStyles({ variant, className })
      )}
    >
      {composeRenderProps(props.children, (children, { isSelected, selectionMode }) => (
        <>
          {selectionMode !== "none" && (
            <span className="flex w-8 shrink-0 items-center justify-center">
              {isSelected && <CheckIcon aria-hidden className="size-4 text-fg-accent" />}
            </span>
          )}
          <span className="flex items-center gap-3">
            {prefix}
            <span className="flex flex-1 flex-col">
              {label && <Text slot="label">{label}</Text>}
              {description && <Text slot="description">{description}</Text>}
              {children}
            </span>
            {suffix}
          </span>
        </>
      ))}
    </AriaListBoxItem>
  );
};

export type { ListBoxProps, ItemProps };
export { ListBox, Item };

Update the import paths to match your project setup.

Usage

Use ListBox to display a list of options and allow a user to select one or more of them.

ListBox Options

Orientation

By default, ListBox expects items to be arranged in a vertical stack, and implements keyboard navigation and drag and drop accordingly. Use the orientation prop to change the layout to a horizontal stack.

<ListBox orientation="horizontal" selectionMode="single">
  <Item textValue="Read" label="Read" description="Read Only" />
  <Item textValue="Write" label="Write" description="Read and Write Only" />
  <Item textValue="Admin" label="Admin" description="Full access" />
</ListBox>

Layout

The layout prop can be set to "grid" to enable two-dimensional keyboard navigation.

<ListBox layout="grid">
  <Item>Next.js</Item>
  <Item>Remix</Item>
  <Item>Astro</Item>
  <Item>Gatsby</Item>
</ListBox>

Selection mode

ListBox supports multiple selection modes. By default, selection is disabled, however this can be changed using the selectionMode prop.

<ListBox selectionMode="single">
  <Item>Next.js</Item>
  <Item>Remix</Item>
  <Item>Astro</Item>
  <Item>Gatsby</Item>
</ListBox>
<ListBox selectionMode="multiple">
  <Item>Next.js</Item>
  <Item>Remix</Item>
  <Item>Astro</Item>
  <Item>Gatsby</Item>
</ListBox>

Selection behavior

When selectionBehavior is set to "replace", clicking a row with the mouse replaces the selection with only that row. Using the arrow keys moves both focus and selection. To select multiple rows, modifier keys such as Ctrl, Cmd, and Shift can be used. On touch screen devices, selection always behaves as toggle since modifier keys may not be available.

<ListBox selectionMode="multiple" selectionBehavior="replace">
  <Item>Next.js</Item>
  <Item>Remix</Item>
  <Item>Astro</Item>
  <Item>Gatsby</Item>
</ListBox>

Loading

Use the isLoading prop to indicate that the list is loading.

<ListBox isLoading>
  <Item>User 1</Item>
  <Item>User 2</Item>
  <Item>User 3</Item>
</ListBox>

Empty state

Use the renderEmptyState prop to customize what the ListBox will display if there are no items.

<ListBox renderEmptyState={() => "No results found."} />

Item options

Variant

Use the variant prop to set the visual style of the item.

<ListBox>
  <Item>View logs</Item>
  <Item>Manage domains</Item>
  <Item>Transfer project</Item>
  <Item variant="danger">Delete project</Item>
</ListBox>

Label and description

By default, items in a ListBox are labeled by their text contents for accessibility. Items also support the "label" and "description" props to separate primary and secondary content.

<ListBox>
  <Item textValue="Read" label="Read" description="Read Only" />
  <Item textValue="Write" label="Write" description="Read and Write Only" />
  <Item textValue="Admin" label="Admin" description="Full access" />
</ListBox>

Prefix and suffix

To add additional context for the item, such as icons, use the prefix and suffix props.

<ListBox>
  <Item label="New file" description="Create a new file" prefix={<PlusSquareIcon />} />
  <Item label="Copy link" description="Copy the file link" prefix={<CopyIcon />} />
  <Item label="Edit file" description="Allows you to edit the file" prefix={<SquarePenIcon />}/>
</ListBox>

Items may be links to another page or website. This can be achieved by passing the href prop to the Item component.

<ListBox>
  <Item href="https://github.com/mehdibha">GitHub</Item>
  <Item href="https://linkedin.com/in/mehdibha">LinkedIn</Item>
  <Item href="https://x.com/mehdibha_" target="_blank">X</Item>
</ListBox>

Disabled

An Item can be disabled with the isDisabled prop.

<ListBox>
  <Item>Next.js</Item>
  <Item>Remix</Item>
  <Item isDisabled>Gatsby</Item>
  <Item>Astro</Item>
</ListBox>

Sections

ListBox supports sections in order to group options. Sections can be used by wrapping groups of items in a Section element.
Sections without a title must provide an aria-label for accessibility.

<ListBox selectionMode="multiple">
  <Section title="Sauces">
    <Item id="signature-sauce">Signature sauce</Item>
    <Item id="bbq-sauce">BBQ sauce</Item>
    <Item id="honey-mustard">Honey mustard</Item>
    <Item id="tartar-sauce">Tartar sauce</Item>
  </Section>
  <Section title="Cheese">
    <Item id="pepperjack">Pepperjack</Item>
    <Item id="mozzarella">Mozzarella</Item>
    <Item id="blue-cheese">Blue cheese</Item>
  </Section>
  <Section title="Extras">
    <Item id="bacon">Bacon</Item>
    <Item id="sauteed-onions">Sauteed onions</Item>
    <Item id="green-pepper">Green pepper</Item>
  </Section>
</ListBox>

Separator

Separators may be added between items or sections in order to create non-labeled groupings.

<ListBox>
  <Item>New...</Item>
  <Item>Badges</Item>
  <Separator />
  <Item>Save</Item>
  <Item>Save as...</Item>
  <Item>Rename...</Item>
  <Separator />
  <Item>Page setup…</Item>
  <Item>Print…</Item>
</ListBox>

Uncontrolled

Use defaultSelectedKey to set a default selected item.

<ListBox
  selectionMode="multiple"
  defaultSelectedKeys={["nextjs", "remix", "astro"]}
>
  <Item id="nextjs">Next.js</Item>
  <Item id="remix">Remix</Item>
  <Item id="astro">Astro</Item>
  <Item id="gatsby">Gatsby</Item>
</ListBox>

Controlled

Use selectedKey and onChange props to control the selected item.

const [selected, setSelected] = React.useState<Selection>(new Set(["nextjs", "remix", "astro"]));
return (
  <ListBox
    selectionMode="multiple"
    selectedKeys={selected}
    onSelectionChange={setSelected}
  >
    <Item id="nextjs">Next.js</Item>
    <Item id="remix">Remix</Item>
    <Item id="astro">Astro</Item>
    <Item id="gatsby">Gatsby</Item>
  </ListBox>
)

Composition

If you need to customize things further, you can drop down to the composition level.

<ListBox selectionMode="multiple">
  <Item>
    <Text slot="label">Next.js</Text>
    <Text slot="description">React-based SSR and static site framework.</Text>
  </Item>
  <Item>
    <Text slot="label">Remix</Text>
    <Text slot="description">Full-stack framework with efficient data loading.</Text>
  </Item>
  <Item>
    <Text slot="label">Astro</Text>
    <Text slot="description">Lightweight static site builder for performance.</Text>
  </Item>
</ListBox>

Examples

Asynchronous loading

This example uses the useAsyncList hook to handle asynchronous loading of data from a server.

const list = useAsyncList<Character>({
  async load({ signal }) {
    const res = await fetch(`https://pokeapi.co/api/v2/pokemon`, { signal })
    const json = (await res.json()) as { results: Character[] }
    return {
      items: json.results,
    }
  }
})

return (
  <ListBox
    aria-label="Pick a Pokemon"
    items={list.items}
    isLoading={list.isLoading}
    selectionMode="single"
  >
    {(item) => <Item id={item.name}>{item.name}</Item>}
  </ListBox>
)

API Reference

ListBox

PropTypeDefaultDescription
selectionBehavior
'toggle' | 'replace'
-
How multiple selection should behave in the collection.
dragAndDropHooks
DragAndDropHooks
-
The drag and drop hooks returned by useDragAndDrop used to enable drag and drop behavior for the ListBox.
renderEmptyState
(props: ListBoxRenderProps) => ReactNode
-
Provides content to display when there are no items in the list.
layout
'stack' | 'grid'
'stack'
Whether the items are arranged in a stack or grid.
orientation
'horizontal' | 'vertical'
'vertical'
The primary orientation of the items. Usually this is the direction that the collection scrolls.
autoFocus
boolean | 'first' | 'last'
-
Whether to auto focus the listbox or an option.
shouldFocusWrap
boolean
-
Whether focus should wrap around when the end/start is reached.
items
Iterable<T>
-
Item objects in the collection.
disabledKeys
Iterable<Key>
-
The item keys that are disabled. These items cannot be selected, focused, or otherwise interacted with.
selectionMode
'none' | 'single' | 'multiple'
-
The type of selection that is allowed in the collection.
disallowEmptySelection
boolean
-
Whether the collection allows empty selection.
selectedKeys
'all' | Iterable<Key>
-
The currently selected keys in the collection (controlled).
defaultSelectedKeys
'all' | Iterable<Key>
-
The initial selected keys in the collection (uncontrolled).
children
ReactNode | (item: object) => ReactNode
-
The contents of the collection.
dependencies
any[]
-
Values that should invalidate the item cache when using dynamic collections.
className
string | (values: ListBoxRenderProps & {defaultClassName: string | undefined}) => string
-
The CSS className for the element. A function may be provided to compute the class based on component state.
style
CSSProperties | (values: ListBoxRenderProps & {defaultStyle: CSSProperties}) => CSSProperties
-
The inline style for the element. A function may be provided to compute the style based on component state.
EventTypeDescription
onAction
(key: Key) => void
Handler that is called when an item is selected.
onSelectionChange
(keys: Selection) => void
Handler that is called when the selection changes.
onFocus
(e: FocusEvent<Target>) => void
Handler that is called when the element receives focus.
onBlur
(e: FocusEvent<Target>) => void
Handler that is called when the element loses focus.
onFocusChange
(isFocused: boolean) => void
Handler that is called when the element's focus status changes.
onScroll
(e: UIEvent<Element>) => void
Handler that is called when a user scrolls.

Item

PropTypeDefaultDescription
id
Key
-
The unique id of the item.
variant
'default' | 'success' | 'warning' | 'danger' | 'accent'
"default"
The visual style of the menu item.
value
object
-
The object value that this item represents. When using dynamic collections, this is set automatically.
textValue
string
-
A string representation of the item's contents, used for features like typeahead.
isDisabled
boolean
-
Whether the item is disabled.
children
ReactNode | (values: ListBoxItemRenderProps & {defaultChildren: ReactNode | undefined}) => ReactNode
-
The children of the component. A function may be provided to alter the children based on component state.
className
string
-
The CSS className for the element.
style
CSSProperties | (values: ListBoxItemRenderProps & {defaultStyle: CSSProperties}) => CSSProperties
-
The inline style for the element. A function may be provided to compute the style based on component state.
href
Href
-
A URL to link to.
hrefLang
string
-
Hints at the human language of the linked URL.
target
HTMLAttributeAnchorTarget
-
The target window for the link.
rel
string
-
The relationship between the linked resource and the current page.
download
boolean | string
-
Causes the browser to download the linked URL. A string may be provided to suggest a file name.
ping
string
-
A space-separated list of URLs to ping when the link is followed.
referrerPolicy
HTMLAttributeReferrerPolicy
-
How much of the referrer to send when following the link.
routerOptions
RouterOptions
-
Options for the configured client side router.
EventTypeDescription
onAction
() => void
Handler that is called when the item is selected.
onHoverStart
(e: HoverEvent) => void
Handler that is called when a hover interaction starts.
onHoverEnd
(e: HoverEvent) => void
Handler that is called when a hover interaction ends.
onHoverChange
(isHovering: boolean) => void
Handler that is called when the hover state changes.

Accessibility

Keyboard interactions

KeyDescription
Tab
Focuses the first item.
ArrowDown
When focus is on an item, moves focus to the next item.
ArrowUp
When focus is on an item, moves focus to the previous item.

Built by mehdibha. The source code is available on GitHub.