ListBox
A listbox displays a list of options and allows a user to select one or more of them
Usage
import { ListBox } from '@lenso/ui';"use client";
// Adapted from HeroUI v3.2.6 list-box-default (Apache-2.0).import * as stylex from "@stylexjs/stylex";import { descriptionStyles } from "@lenso/tokens/description";import { labelStyles } from "@lenso/tokens/label";import { Avatar, ListBox, ListBoxItem } from "@lenso/ui";
const users = [ { key: "1", textValue: "Bob", color: "blue" }, { key: "2", textValue: "Fred", color: "green" }, { key: "3", textValue: "Martha", color: "purple" },] as const;const styles = stylex.create({ root: { width: 220 }, details: { display: "flex", flexDirection: "column" },});
export function Default() { return ( <ListBox aria-label="Users" xstyle={styles.root} selectionMode="single"> {users.map((user) => ( <ListBoxItem key={user.key} itemKey={user.key} textValue={user.textValue}> <Avatar size="sm"> <Avatar.Image alt={user.textValue} src={`https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/${user.color}.jpg`} /> <Avatar.Fallback>{user.textValue[0]}</Avatar.Fallback> </Avatar> <div {...stylex.props(styles.details)}> <span {...stylex.props(labelStyles.label)}>{user.textValue}</span> <span {...stylex.props(descriptionStyles.description)}> {user.textValue.toLowerCase()}@heroui.com </span> </div> <ListBoxItem.Indicator /> </ListBoxItem> ))} </ListBox> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
Anatomy
import { ListBox, Label, Description, Header } from '@lenso/ui';
export default () => ( <ListBox> <ListBox.Item> <Label /> <Description /> <ListBox.ItemIndicator /> </ListBox.Item> <ListBox.Section> <Header /> <ListBox.Item> <Label /> </ListBox.Item> </ListBox.Section> </ListBox>)Examples
With Disabled Items
"use client";// HeroUI v3.2.6 with-disabled-items adaptation (Apache-2.0).import { Actions } from "./actions";export function WithDisabledItems() { return <Actions disabled />;}Local adaptation source above. Derived from HeroUI v3.2.6 source.
With Sections
"use client";// HeroUI v3.2.6 with-sections adaptation (Apache-2.0).import { Actions } from "./actions";export function WithSections() { return <Actions />;}Local adaptation source above. Derived from HeroUI v3.2.6 source.
Multi Select
"use client";// HeroUI v3.2.6 multi-select adaptation (Apache-2.0).import * as stylex from "@stylexjs/stylex";import { UsersList, styles } from "./users";export function MultiSelect() { return ( <div {...stylex.props(styles.surface)}> <UsersList selectionMode="multiple" /> </div> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
Controlled
"use client";// HeroUI v3.2.6 controlled adaptation (Apache-2.0).import { useState } from "react";import * as stylex from "@stylexjs/stylex";import { UsersList, styles } from "./users";export function Controlled() { const [selected, setSelected] = useState<Set<React.Key>>(new Set(["1"])); return ( <div {...stylex.props(styles.stack)}> <div {...stylex.props(styles.surface)}> <UsersList selectionMode="multiple" selectedKeys={selected} onSelectionChange={setSelected} customCheck /> </div> <p {...stylex.props(styles.muted)}> Selected: {selected.size ? [...selected].join(", ") : "None"} </p> </div> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
Virtualization
ListBox supports virtualization through Virtualizer, enabling efficient rendering of large datasets by displaying only the rows visible within the viewport.
"use client";// HeroUI v3.2.6 virtualization adaptation (Apache-2.0); native fixed-row window.import * as stylex from "@stylexjs/stylex";import { ListBox, ListBoxItem } from "@lenso/ui";import { Description, Label } from "./text";const first = [ "Emma", "Liam", "Olivia", "Noah", "Ava", "James", "Sophia", "Oliver", "Isabella", "Lucas", "Mia", "Ethan", "Charlotte", "Mason", "Amelia", "Logan", "Harper", "Alexander", "Ella", "Benjamin",];const last = [ "Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller", "Davis", "Rodriguez", "Martinez", "Anderson", "Taylor", "Thomas", "Jackson", "White", "Harris", "Clark", "Lewis", "Robinson", "Walker",];const users = Array.from({ length: 1000 }, (_, index) => ({ key: index + 1, textValue: `${first[index % 20]} ${last[Math.floor(index / 20) % 20]}`, email: `${first[index % 20]!.toLowerCase()}.${last[Math.floor(index / 20) % 20]!.toLowerCase()}@acme.com`,}));const styles = stylex.create({ list: { width: 300 }, detail: { display: "flex", flexDirection: "column" },});export function Virtualization() { return ( <ListBox aria-label="Virtualized list with 1000 items" xstyle={styles.list} items={users} virtualized={{ rowHeight: 50, height: 400 }} > {(item) => ( <ListBoxItem itemKey={item.key} textValue={item.textValue}> <div {...stylex.props(styles.detail)}> <Label>{item.textValue}</Label> <Description>{users[Number(item.key) - 1]!.email}</Description> </div> <ListBoxItem.Indicator /> </ListBoxItem> )} </ListBox> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
Custom Check Icon
"use client";// HeroUI v3.2.6 custom-check-icon adaptation (Apache-2.0).import * as stylex from "@stylexjs/stylex";import { UsersList, styles } from "./users";export function CustomCheckIcon() { return ( <div {...stylex.props(styles.surface)}> <UsersList selectionMode="multiple" customCheck /> </div> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
Render Function
"use client";// HeroUI v3.2.6 render-function adaptation (Apache-2.0).import { UsersList, styles } from "./users";export function RenderFunction() { return ( <UsersList xstyle={styles.list} selectionMode="single" render={(props) => <div {...props} data-custom="true" />} renderItems /> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
Customization
Tailwind CSS
"use client";// HeroUI v3.2.6 custom-styles adaptation (Apache-2.0).import { UsersList, styles } from "./users";export function CustomStyles() { return ( <UsersList aria-label="Assignee" xstyle={styles.custom} selectionMode="single" customStyles count={2} /> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
Global CSS
To customize the ListBox component classes, you can use the @layer components directive.
Learn more.
@layer components { .list-box { @apply rounded-lg border border-border bg-surface p-2; }
.list-box-item { @apply rounded px-2 py-1 cursor-pointer; }
.list-box-item--danger { @apply text-danger; }
.list-box-item__indicator { @apply text-accent; }}Styling Reference
HeroUI follows the BEM methodology to ensure component variants and states are reusable and easy to customize.
CSS Classes
The ListBox component uses these CSS classes (View source styles):
Base Classes [!toc]
.list-box- Base listbox container.list-box-item- Individual listbox item.list-box-item__indicator- Selection indicator icon.list-box-section- Section container for grouping items
Variant Classes [!toc]
.list-box--default- Default variant styling.list-box--danger- Danger variant styling.list-box-item--default- Default item variant.list-box-item--danger- Danger item variant
State Classes [!toc]
.list-box-item[data-selected="true"]- Selected item state.list-box-item[data-focus-visible="true"]- Focused item state.list-box-item[data-disabled="true"]- Disabled item state.list-box-item__indicator[data-visible="true"]- Visible indicator state
Interactive States
The component supports both CSS pseudo-classes and data attributes for flexibility:
- Hover:
:hoveror[data-hovered="true"]on item - Focus:
:focus-visibleor[data-focus-visible="true"]on item - Selected:
[data-selected="true"]on item - Disabled:
:disabledor[data-disabled="true"]on item
API Reference
ListBox
| Prop | Type | Default | Description |
|---|---|---|---|
aria-label | string | - | Accessibility label for the listbox |
aria-labelledby | string | - | ID of element that labels the listbox |
selectionMode | "none" | "single" | "multiple" | "single" | Selection behavior |
selectedKeys | Selection | - | Controlled selected keys |
defaultSelectedKeys | Selection | - | Initial selected keys |
onSelectionChange | (keys: Selection) => void | - | Handler called when selection changes |
disabledKeys | Iterable<Key> | - | Keys of disabled items |
onAction | (key: Key) => void | - | Handler called when an item is activated |
variant | "default" | "danger" | "default" | Visual variant |
className | string | - | Additional CSS classes |
children | ReactNode | - | ListBox items and sections |
render | DOMRenderFunction<keyof React.JSX.IntrinsicElements, ListBoxRenderProps> | - | Overrides the default DOM element with a custom render function. |
ListBox.Item
| Prop | Type | Default | Description |
|---|---|---|---|
id | Key | - | Unique identifier for the item |
textValue | string | - | Text value for accessibility and typeahead |
isDisabled | boolean | false | Whether this item is disabled |
variant | "default" | "danger" | "default" | Visual variant |
className | string | - | Additional CSS classes |
children | ReactNode | RenderFunction | - | Item content or render function |
render | (props: DetailedHTMLProps<LinkWithRequiredHref, HTMLAnchorElement> | React.JSX.IntrinsicElements[keyof React.JSX.IntrinsicElements], renderProps: ListBoxItemRenderProps) => ReactElement | - | Overrides the default DOM element with a custom render function. |
ListBox.ItemIndicator
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Additional CSS classes |
children | ReactNode | RenderFunction | - | Custom indicator content or render function |
ListBox.Section
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Additional CSS classes |
children | ReactNode | - | Section content including Header and Items |
Render Props
When using render functions with ListBox.Item or ListBox.ItemIndicator, these values are provided:
| Prop | Type | Description |
|---|---|---|
isSelected | boolean | Whether the item is selected |
isFocused | boolean | Whether the item is focused |
isDisabled | boolean | Whether the item is disabled |
isPressed | boolean | Whether the item is being pressed |
ListLayout
| Name | Type | Default | Description |
|---|---|---|---|
rowHeight | number | undefined | 48 | The fixed height of a row in px. |
estimatedRowHeight | number | undefined | — | The estimated height of a row, when row heights are variable. |
headingHeight | number | undefined | 48 | The fixed height of a section header in px. |
estimatedHeadingHeight | number | undefined | — | The estimated height of a section header, when the height is variable. |
loaderHeight | number | undefined | 48 | The fixed height of a loader element in px. This loader is specifically for "load more" elements rendered when loading more rows at the root level or inside nested row/sections. |
dropIndicatorThickness | number | undefined | 2 | The thickness of the drop indicator. |
gap | number | undefined | 0 | The gap between items. |
padding | number | undefined | 0 | The padding around the list. |
Examples
Basic Usage
import { ListBox, Label, Description } from '@lenso/ui';
<ListBox aria-label="Users" selectionMode="single"> <ListBox.Item id="1" textValue="Bob"> <Label>Bob</Label> <Description>[email protected]</Description> </ListBox.Item> <ListBox.Item id="2" textValue="Alice"> <Label>Alice</Label> <Description>[email protected]</Description> </ListBox.Item></ListBox>With Sections
import { ListBox, Header, Separator } from '@lenso/ui';
<ListBox aria-label="Actions" selectionMode="none" onAction={(key) => console.log(key)}> <ListBox.Section> <Header>Actions</Header> <ListBox.Item id="new" textValue="New file">New file</ListBox.Item> <ListBox.Item id="edit" textValue="Edit file">Edit file</ListBox.Item> </ListBox.Section> <Separator /> <ListBox.Section> <Header>Danger zone</Header> <ListBox.Item id="delete" textValue="Delete" variant="danger">Delete</ListBox.Item> </ListBox.Section></ListBox>Controlled Selection
import { ListBox, Selection } from '@lenso/ui';import { useState } from 'react';
function ControlledListBox() { const [selected, setSelected] = useState<Selection>(new Set(["1"]));
return ( <ListBox aria-label="Options" selectedKeys={selected} selectionMode="multiple" onSelectionChange={setSelected} > <ListBox.Item id="1" textValue="Option 1">Option 1</ListBox.Item> <ListBox.Item id="2" textValue="Option 2">Option 2</ListBox.Item> <ListBox.Item id="3" textValue="Option 3">Option 3</ListBox.Item> </ListBox> );}Custom Indicator
import { ListBox, ListBoxItemIndicator } from '@lenso/ui';import { Icon } from '@iconify/react';
<ListBox aria-label="Options" selectionMode="multiple"> <ListBox.Item id="1" textValue="Option 1"> Option 1 <ListBox.ItemIndicator> {({isSelected}) => isSelected ? <Icon icon="gravity-ui:check" /> : null } </ListBox.ItemIndicator> </ListBox.Item></ListBox>Accessibility
The ListBox component implements the ARIA listbox pattern and provides:
- Full keyboard navigation support
- Screen reader announcements for selection changes
- Proper focus management
- Support for disabled states
- Typeahead search functionality
For more information, see the React Aria ListBox documentation.