Listbox
Migration guide for Listbox (renamed to ListBox with a capital "B") from HeroUI v2 to v3
Refer to the v3 ListBox documentation for complete API reference, styling guide, and advanced examples. This guide only focuses on migrating from HeroUI v2.
Structure Changes
v2: Separate Components
In v2, Listbox used separate components:
import { Listbox, ListboxItem, ListboxSection } from "@heroui/react";
export default function App() {
return (
<Listbox>
<ListboxItem key="1">Item 1</ListboxItem>
</Listbox>
);
}v3: Compound Components
In v3, ListBox uses compound components:
import { ListBox, Label } from "@heroui/react";
export default function App() {
return (
<ListBox>
<ListBox.Item id="1" textValue="Item 1">
<Label>Item 1</Label>
</ListBox.Item>
</ListBox>
);
}Key Changes
1. Component Naming
v2: Listbox, ListboxItem, ListboxSection
v3: ListBox, ListBox.Item, ListBox.Section
2. Item Identification
v2: React's key was used for both list reconciliation and item identity (selection, focus).
v3: Use id for state/focus and textValue for accessibility (when content isn't plain text); keep React's key on items in lists.
3. Prop Changes
| v2 Prop | v3 Location | Notes |
|---|---|---|
key (for state) | ListBox.Item | Use id for item identity (state) |
| - | textValue (on ListBox.Item) | For accessibility (type-ahead) |
variant, color | variant on ListBox or ListBox.Item | Simplified to "default" | "danger" (no color prop) |
onAction | ListBox | Callback fired when an item is pressed ((key: Key) => void) |
disabledKeys | ListBox | Same — set of keys for items that should be non-interactive |
startContent, endContent | - | Place icons manually in item content |
description | Description | Use Description component |
title (on Section) | Header | Use Header component |
topContent, bottomContent | - | Removed (handle separately) |
itemClasses, classNames | - | Use className on parts |
hideSelectedIcon | - | Omit ListBox.ItemIndicator |
disableAnimation | - | Removed |
isVirtualized, virtualization | React Aria Virtualizer | Use React Aria's <Virtualizer> wrapper (see example below) |
selectedKeys | ListBox | Same (uses Selection type Set) |
Migration Examples
Selection
import { useState } from "react";
{/* Single selection */}
const [singleSelected, setSingleSelected] = useState(new Set(["text"]));
<Listbox
selectedKeys={singleSelected}
selectionMode="single"
onSelectionChange={setSingleSelected}
>
<ListboxItem key="text">Text</ListboxItem>
<ListboxItem key="number">Number</ListboxItem>
</Listbox>
{/* Multiple selection */}
const [multiSelected, setMultiSelected] = useState(new Set(["text"]));
<Listbox
selectedKeys={multiSelected}
selectionMode="multiple"
onSelectionChange={setMultiSelected}
>
<ListboxItem key="text">Text</ListboxItem>
<ListboxItem key="number">Number</ListboxItem>
</Listbox>import { useState } from "react";
import type { Selection } from "@heroui/react";
{/* Single selection */}
const [singleSelected, setSingleSelected] = useState<Selection>(new Set(["text"]));
<ListBox
selectedKeys={singleSelected}
selectionMode="single"
onSelectionChange={setSingleSelected}
>
<ListBox.Item id="text" textValue="Text">
<Label>Text</Label>
<ListBox.ItemIndicator />
</ListBox.Item>
<ListBox.Item id="number" textValue="Number">
<Label>Number</Label>
<ListBox.ItemIndicator />
</ListBox.Item>
</ListBox>
{/* Multiple selection */}
const [multiSelected, setMultiSelected] = useState<Selection>(new Set(["text"]));
<ListBox
selectedKeys={multiSelected}
selectionMode="multiple"
onSelectionChange={setMultiSelected}
>
<ListBox.Item id="text" textValue="Text">
<Label>Text</Label>
<ListBox.ItemIndicator />
</ListBox.Item>
<ListBox.Item id="number" textValue="Number">
<Label>Number</Label>
<ListBox.ItemIndicator />
</ListBox.Item>
</ListBox>With Description
<ListboxItem
key="new"
description="Create a new file"
>
New file
</ListboxItem>import { Description, Label } from "@heroui/react";
<ListBox.Item id="new" textValue="New file">
<Label>New file</Label>
<Description>Create a new file</Description>
</ListBox.Item>With Icons
<ListboxItem
key="new"
startContent={<AddIcon />}
>
New file
</ListboxItem><ListBox.Item id="new" textValue="New file">
<AddIcon />
<Label>New file</Label>
</ListBox.Item>With Sections
<Listbox>
<ListboxSection title="Actions">
<ListboxItem key="new">New file</ListboxItem>
<ListboxItem key="edit">Edit file</ListboxItem>
</ListboxSection>
<ListboxSection title="Danger zone">
<ListboxItem key="delete">Delete</ListboxItem>
</ListboxSection>
</Listbox>import { Header, Label, Separator } from "@heroui/react";
<ListBox>
<ListBox.Section>
<Header>Actions</Header>
<ListBox.Item id="new" textValue="New file">
<Label>New file</Label>
</ListBox.Item>
<ListBox.Item id="edit" textValue="Edit file">
<Label>Edit file</Label>
</ListBox.Item>
</ListBox.Section>
<Separator />
<ListBox.Section>
<Header>Danger zone</Header>
<ListBox.Item id="delete" textValue="Delete" variant="danger">
<Label>Delete</Label>
</ListBox.Item>
</ListBox.Section>
</ListBox>With Custom Indicator
<ListboxItem
key="1"
selectedIcon={<CustomCheckIcon />}
>
Item 1
</ListboxItem><ListBox.Item id="1" textValue="Item 1">
<Label>Item 1</Label>
<ListBox.ItemIndicator>
{({isSelected}) =>
isSelected ? <CustomCheckIcon /> : null
}
</ListBox.ItemIndicator>
</ListBox.Item>Variant Prop
In v3, both ListBox and ListBox.Item accept a variant prop with values "default" (default) or "danger". Setting variant on the root ListBox applies to all items; setting it on an individual ListBox.Item overrides the root value for that item.
{/* Root-level variant — all items inherit "danger" styling */}
<ListBox variant="danger">
<ListBox.Item id="delete" textValue="Delete">
<Label>Delete</Label>
</ListBox.Item>
</ListBox>
{/* Per-item variant */}
<ListBox>
<ListBox.Item id="edit" textValue="Edit">
<Label>Edit</Label>
</ListBox.Item>
<ListBox.Item id="delete" textValue="Delete" variant="danger">
<Label>Delete</Label>
</ListBox.Item>
</ListBox>onAction Handler
The onAction callback is fired when an item is pressed (click or Enter). It receives the item's id as a Key.
<ListBox onAction={(key) => alert(`Action on ${key}`)}>
<ListBox.Item id="copy" textValue="Copy">
<Label>Copy</Label>
</ListBox.Item>
<ListBox.Item id="paste" textValue="Paste">
<Label>Paste</Label>
</ListBox.Item>
</ListBox>Disabled Keys
Use disabledKeys to make specific items non-interactive:
<ListBox disabledKeys={new Set(["paste"])}>
<ListBox.Item id="copy" textValue="Copy">
<Label>Copy</Label>
</ListBox.Item>
<ListBox.Item id="paste" textValue="Paste">
<Label>Paste</Label>
</ListBox.Item>
</ListBox>Render Props on ListBox.Item
ListBox.Item supports render props, giving you access to the current interaction state. The available render prop values are isSelected, isFocused, isDisabled, and isPressed:
<ListBox selectionMode="single">
<ListBox.Item id="item1" textValue="Item 1">
{({isSelected, isFocused, isDisabled, isPressed}) => (
<>
<Label className={isSelected ? "font-bold" : ""}>Item 1</Label>
{isSelected && <ListBox.ItemIndicator />}
</>
)}
</ListBox.Item>
</ListBox>Virtualization
Virtualization is still supported in v3 via React Aria's <Virtualizer> component. Wrap your ListBox items with Virtualizer for efficient rendering of large lists:
import {Virtualizer} from "react-aria-components";
<ListBox
aria-label="Large list"
items={items}
selectionMode="multiple"
>
<Virtualizer>
{(item) => (
<ListBox.Item id={item.id} textValue={item.name}>
<Label>{item.name}</Label>
</ListBox.Item>
)}
</Virtualizer>
</ListBox>Component Anatomy
The v3 ListBox follows this structure:
ListBox (Root)
├── ListBox.Item
│ ├── Icon (optional, manual placement)
│ ├── Label (required)
│ ├── Description (optional)
│ └── ListBox.ItemIndicator (optional)
└── ListBox.Section (optional)
├── Header (optional)
└── ListBox.ItemSummary
- Component Naming:
Listbox→ListBox,ListboxItem→ListBox.Item,ListboxSection→ListBox.Section - Item Structure: Must use
Label,Description,ListBox.ItemIndicatorcomponents - Icons: Manual placement instead of
startContent/endContentprops - Sections: Use
Headercomponent instead oftitleprop - Variant Prop:
variantandcolorreplaced by a singlevariantprop ("default"|"danger") on bothListBoxandListBox.Item - onAction: New
onActioncallback onListBoxfor item press handling - disabledKeys: Supported on
ListBoxto disable specific items - Render Props:
ListBox.ItemprovidesisSelected,isFocused,isDisabled,isPressedvia render props - Content Props Removed:
topContent,bottomContent- handle separately - Virtualization: Still supported via React Aria's
<Virtualizer>component (replacesisVirtualizedprop) - Selection Type: Uses
Selectiontype (Set) instead of arrays