import { Meta, Canvas, Story } from '@storybook/addon-docs/blocks'; import { ListBox } from './ListBox'; import * as ListBoxStories from './ListBox.stories';
A versatile list selection component that allows users to select one or more items from a list of options. Built with React Aria's accessibility features and the Cube tasty style system, it supports sections, descriptions, keyboard navigation, and virtualization for large datasets.
- Present a list of selectable options in a contained area
- Enable single or multiple selection from a set of choices
- Display structured data with sections and descriptions
- Create custom selection interfaces that need to remain visible
- Build form controls that require persistent option visibility
- Handle lists of any size (virtualization is automatic for flat lists)
- When you need rich option content (icons, descriptions, badges, hotkeys)
- For multiple selection with clear visual feedback (checkboxes)
- When options should always be visible (for searchable lists, consider FilterListBox)
selectedKeystring— The selected key in controlled modedefaultSelectedKeystring— The default selected key in uncontrolled modeselectedKeysstring[] | 'all'— The selected keys in controlled multiple mode. Use "all" to select all items or an array of keys.defaultSelectedKeysstring[] | 'all'— The default selected keys in uncontrolled multiple mode. Use "all" to select all items or an array of keys.selectionMode'single' | 'multiple'(default:single) — Selection modedisallowEmptySelectionboolean(default:false) — Whether to disallow empty selectiondisabledKeysKey[]— Array of keys for disabled itemssize'small' | 'medium' | 'large'(default:medium) — ListBox sizeshape'card' | 'plain' | 'popover'(default:card) — Visual shape of the ListBoxfilter(nodes: Iterable<Node>) => Iterable<Node>— Filter function for the list itemsemptyLabelReactNode(default:No items) — Label shown when no items are availableheaderReactNode— Custom header contentfooterReactNode— Custom footer contentfocusOnHoverboolean(default:true) — Whether moving the pointer over an option will move DOM focus to that optionshouldUseVirtualFocusboolean(default:false) — Whether to use virtual focus instead of DOM focusisCheckableboolean(default:false) — Whether to show checkboxes for multiple selection modeshouldFocusWrapboolean(default:false) — Whether keyboard navigation should wrap arounddescriptionstring— Field descriptionmessagestring— Help or error messageshowSelectAllboolean(default:false) — Whether to show the "Select All" option in multiple selection modeselectAllLabelstring(default:Select All) — Label for the "Select All" optiononSelectionChange(keys: Key | Key[] | 'all' | null) => void— Callback when selection changesonEscape() => void— Callback when Escape key is pressedonOptionClick(key: Key) => void— Callback when an option is clicked (non-checkbox area)itemsIterable<T>— Array of items for dynamic content with render function patterndisableSelectionToggleboolean(default:false) — When true, clicking an already-selected item keeps it selected instead of toggling it off
Supports Base properties
Supports all Field properties
Customizes the root wrapper element of the component.
Sub-elements:
ValidationState- Container for validation and loading indicators
Customizes the list container element.
Customizes individual option elements.
Sub-elements:
Label- The main text of each optionDescription- Secondary descriptive text for optionsContent- Container for label and descriptionCheckbox- Checkbox element whenisCheckable={true}CheckboxWrapper- Wrapper around the checkbox
Customizes section wrapper elements.
Customizes section heading elements.
Customizes the header area when header prop is provided.
Customizes the footer area when footer prop is provided.
These properties allow direct style application without using the styles prop:
- Base:
display,font,preset,hide,whiteSpace,opacity,transition - Position:
gridArea,order,gridColumn,gridRow,placeSelf,alignSelf,justifySelf,zIndex,margin,inset,position - Dimension:
width,height,flexBasis,flexGrow,flexShrink,flex - Block:
border,radius,shadow,outline - Color:
color,fill,fade,image
The mods property accepts the following modifiers you can override:
invalidboolean— Applied whenvalidationState="invalid"validboolean— Applied whenvalidationState="valid"disabledboolean— Applied whenisDisabled={true}focusedboolean— Applied when the ListBox has focusheaderboolean— Applied when header prop is provided orshowSelectAll={true}footerboolean— Applied when footer prop is providedselectAllboolean— Applied whenshowSelectAll={true}in multiple selection mode
isCheckable (boolean, default: false)
- When
truein multiple selection mode, displays checkboxes on the left of each option - Checkboxes are visible when the item is hovered, focused, or selected
- Improves clarity of multiple selection interactions
showSelectAll (boolean, default: false)
- When
truein multiple selection mode, displays a "Select All" option in the header - The checkbox shows three states: unchecked (none selected), indeterminate (some selected), checked (all selected)
- Only works with
selectionMode="multiple"
selectAllLabel (ReactNode, default: 'Select All')
- Custom label for the "Select All" option
- Only used when
showSelectAll={true}
allValueProps (Partial<CubeItemProps>)
- Props to customize the styling and behavior of the "Select All" option
- Accepts any Item props like
styles,icon, etc.
disallowEmptySelection (boolean, default: false)
- When
true, prevents the user from deselecting the last selected item - Ensures at least one item is always selected in single selection mode
disableSelectionToggle (boolean, default: false)
- When
true, clicking an already-selected item keeps it selected instead of toggling it off - Useful when embedding ListBox inside components like ComboBox
disabledKeys (Key[])
- Array of keys for items that should be disabled
- Disabled items cannot be selected and are visually distinguished
shape ('card' | 'plain' | 'popover', default: 'card')
- Controls the visual styling of the ListBox container
card: Standard card styling with border and margin (default)plain: No border, no margin, no radius - suitable for embedded usepopover: No border, but keeps margin and radius - suitable for overlay use- Use
plainwhen embedding ListBox directly into another component - Use
popoverwhen using ListBox inside overlays (Dialog, ComboBox, Picker)
focusOnHover (boolean, default: true)
- When
true, moving the pointer over an option will move DOM focus to that option - Set to
falsefor components that keep DOM focus outside (e.g., searchable FilterListBox)
shouldUseVirtualFocus (boolean, default: false)
- When
true, uses virtual focus for keyboard navigation - DOM focus stays outside individual option elements (useful for searchable lists)
shouldFocusWrap (boolean, default: false)
- When
true, keyboard navigation wraps around when reaching the end of the list
onSelectionChange ((key: Key | null | 'all' | Key[]) => void)
- Callback fired when selection changes
- In single mode: receives a single key or
null - In multiple mode: receives an array of keys or
'all'for select all
onEscape (() => void)
- Callback fired when the user presses Escape key
- When provided, prevents React Aria's default Escape behavior (selection reset)
- Useful for closing parent overlays
onOptionClick ((key: Key) => void)
- Callback fired when an option is clicked (not on the checkbox area in checkable mode)
- Used by FilterPicker to close the popover on non-checkbox clicks
stateRef (RefObject<any>)
- Ref to access the internal ListState instance
- Allows parent components to access selection state and other list functionality
listRef (RefObject<HTMLUListElement>)
- Ref for accessing the list DOM element
Individual items within the ListBox. Each item is rendered using Item and supports all Item properties for layout, icons, descriptions, and interactive features.
ListBox.Item is built using Item, which provides rich layout and interaction capabilities. All Item properties are supported:
keystring \| number— Unique identifier for the item (required)childrenReactNode— The main content/label for the optiontextValuestring— Text representation for complex JSX content (for screen readers and search)iconReactNode— Icon displayed before the contentrightIconReactNode— Icon displayed after the contentdescriptionReactNode— Secondary text below the main contentdescriptionPlacement'inline' \| 'block'(default:'block') — How the description is positionedprefixReactNode— Content displayed at the start (before icon)suffixReactNode— Content displayed at the end (after content)hotkeysstring— Keyboard shortcut hint (e.g.,'ctrl+a') displayed as suffixtooltipstring \| object— Tooltip shown on hover. Object:{ title, description, placement }stylesStyles— Custom styling for the itemqastring— QA identifier for testing
<ListBox label="Team Members" selectionMode="multiple">
<ListBox.Item
key="user1"
icon={<IconUser />}
description="Product Manager"
suffix="Online"
>
Alice Johnson
</ListBox.Item>
<ListBox.Item
key="user2"
icon={<IconUser />}
description="Senior Developer"
rightIcon={<IconBadge />}
>
Bob Smith
</ListBox.Item>
</ListBox>Groups related items together with an optional heading.
titleReactNode— Optional heading text for the sectionchildrenListBox.Item[]— Collection of ListBox.Item components
single- Allows selecting only one item at a timemultiple- Allows selecting multiple itemsnone- No selection allowed (display only)
small- Compact size for dense interfaces (28px item height)medium- Standard size for general use (32px item height, default)large- Emphasized size for important sections (40px item height)
<ListBox label="Select a fruit" selectionMode="single">
<ListBox.Item key="apple">Apple</ListBox.Item>
<ListBox.Item key="banana">Banana</ListBox.Item>
<ListBox.Item key="cherry">Cherry</ListBox.Item>
</ListBox><ListBox label="Choose a framework" selectionMode="single">
<ListBox.Item
key="react"
description="A JavaScript library for building user interfaces"
>
React
</ListBox.Item>
<ListBox.Item
key="vue"
description="The Progressive JavaScript Framework"
>
Vue.js
</ListBox.Item>
</ListBox><ListBox label="Select food items" selectionMode="single">
<ListBox.Section title="Fruits">
<ListBox.Item key="apple">Apple</ListBox.Item>
<ListBox.Item key="banana">Banana</ListBox.Item>
</ListBox.Section>
<ListBox.Section title="Vegetables">
<ListBox.Item key="carrot">Carrot</ListBox.Item>
<ListBox.Item key="broccoli">Broccoli</ListBox.Item>
</ListBox.Section>
</ListBox>Sections and items can be defined dynamically using the items prop with a render function. Pass hierarchical data where each section contains a children array.
const categories = [
{ name: 'Fruits', children: [
{ key: 'apple', label: 'Apple' },
{ key: 'banana', label: 'Banana' },
]},
{ name: 'Vegetables', children: [
{ key: 'carrot', label: 'Carrot' },
{ key: 'broccoli', label: 'Broccoli' },
]},
];
<ListBox label="Select food items" selectionMode="single" items={categories}>
{(category) => (
<ListBox.Section key={category.name} title={category.name} items={category.children}>
{(item) => <ListBox.Item key={item.key}>{item.label}</ListBox.Item>}
</ListBox.Section>
)}
</ListBox><ListBox
label="Select skills (multiple)"
selectionMode="multiple"
>
<ListBox.Item key="html">HTML</ListBox.Item>
<ListBox.Item key="css">CSS</ListBox.Item>
<ListBox.Item key="javascript">JavaScript</ListBox.Item>
</ListBox><ListBox
label="Select permissions"
selectionMode="multiple"
isCheckable={true}
>
<ListBox.Item key="read" description="View content and data">Read</ListBox.Item>
<ListBox.Item key="write" description="Create and edit content">Write</ListBox.Item>
<ListBox.Item key="delete" description="Remove content permanently">Delete</ListBox.Item>
</ListBox><ListBox
label="Select permissions"
selectionMode="multiple"
isCheckable={true}
showSelectAll={true}
selectAllLabel="All Permissions"
>
<ListBox.Item key="read" description="View content and data">Read</ListBox.Item>
<ListBox.Item key="write" description="Create and edit content">Write</ListBox.Item>
<ListBox.Item key="execute" description="Execute operations">Execute</ListBox.Item>
</ListBox><ListBox
label="Programming Languages"
header={
<Space gap="1x" flow="row" placeItems="center">
<Title level={6}>Languages</Title>
<Badge type="note">12</Badge>
</Space>
}
footer={
<Text color="#dark.50" preset="t4">
Popular languages shown
</Text>
}
>
<ListBox.Item key="javascript">JavaScript</ListBox.Item>
<ListBox.Item key="python">Python</ListBox.Item>
</ListBox>const [selectedKey, setSelectedKey] = useState('apple');
<ListBox
label="Controlled ListBox"
selectedKey={selectedKey}
onSelectionChange={setSelectedKey}
selectionMode="single"
>
<ListBox.Item key="apple">Apple</ListBox.Item>
<ListBox.Item key="banana">Banana</ListBox.Item>
</ListBox><ListBox
label="Select an option"
selectionMode="single"
disabledKeys={['disabled1', 'disabled2']}
>
<ListBox.Item key="available1">Available Option 1</ListBox.Item>
<ListBox.Item key="disabled1">Disabled Option 1</ListBox.Item>
<ListBox.Item key="available2">Available Option 2</ListBox.Item>
</ListBox><ListBox
label="Must select one option"
selectionMode="single"
disallowEmptySelection={true}
defaultSelectedKey="apple"
>
<ListBox.Item key="apple">Apple</ListBox.Item>
<ListBox.Item key="banana">Banana</ListBox.Item>
</ListBox><ListBox label="User Roles" selectionMode="single" size="large">
<ListBox.Item
key="admin"
description="Full system administration access"
prefix={<Badge type="danger">Admin</Badge>}
suffix={<Badge type="note">3</Badge>}
rightIcon={<SettingsIcon />}
hotkeys="ctrl+a"
>
System Administrator
</ListBox.Item>
<ListBox.Item
key="editor"
description="Content creation and editing permissions"
prefix={<Badge type="warning">Editor</Badge>}
suffix={<Badge type="note">12</Badge>}
rightIcon={<EditIcon />}
hotkeys="ctrl+e"
>
Content Editor
</ListBox.Item>
</ListBox><ListBox label="Project Actions" selectionMode="single">
<ListBox.Item
key="create"
tooltip="Create a new project with default settings"
icon={<PlusIcon />}
>
Create Project
</ListBox.Item>
<ListBox.Item
key="import"
tooltip={{
title: 'Import Project',
description: 'Import an existing project from file or URL',
placement: 'right',
}}
icon={<DatabaseIcon />}
>
Import Project
</ListBox.Item>
</ListBox><ListBox label="Choose your plan" selectionMode="single">
<ListBox.Item
key="basic"
textValue="Basic Plan - Free with limited features"
>
<Space gap="1x" flow="column">
<Text weight="600">Basic Plan</Text>
<Badge type="neutral">Free</Badge>
</Space>
</ListBox.Item>
<ListBox.Item
key="pro"
textValue="Pro Plan - Monthly subscription with all features"
>
<Space gap="1x" flow="column">
<Text weight="600">Pro Plan</Text>
<Badge type="purple">$19/month</Badge>
</Space>
</ListBox.Item>
</ListBox><ListBox size="small" label="Small Size">
<ListBox.Item key="apple">Apple</ListBox.Item>
<ListBox.Item key="banana">Banana</ListBox.Item>
</ListBox>
<ListBox size="medium" label="Medium Size (Default)">
<ListBox.Item key="apple">Apple</ListBox.Item>
<ListBox.Item key="banana">Banana</ListBox.Item>
</ListBox>
<ListBox size="large" label="Large Size">
<ListBox.Item key="apple">Apple</ListBox.Item>
<ListBox.Item key="banana">Banana</ListBox.Item>
</ListBox>{/* Card shape (default) - with border and margin */}
<ListBox shape="card" label="Select a fruit" selectionMode="single">
<ListBox.Item key="apple">Apple</ListBox.Item>
<ListBox.Item key="banana">Banana</ListBox.Item>
</ListBox>
{/* Plain shape - no border, no margin, no radius */}
<ListBox shape="plain" label="Select a fruit" selectionMode="single">
<ListBox.Item key="apple">Apple</ListBox.Item>
<ListBox.Item key="banana">Banana</ListBox.Item>
</ListBox>
{/* Popover shape - no border, but keeps margin and radius */}
<ListBox shape="popover" label="Select a fruit" selectionMode="single">
<ListBox.Item key="apple">Apple</ListBox.Item>
<ListBox.Item key="banana">Banana</ListBox.Item>
</ListBox>const [selectedKey, setSelectedKey] = useState('apple');
<ListBox
label="Custom Escape Handling"
selectedKey={selectedKey}
selectionMode="single"
onSelectionChange={setSelectedKey}
onEscape={() => {
// Custom escape behavior - could close a parent modal, etc.
console.log('Escape pressed');
}}
>
<ListBox.Item key="apple">Apple</ListBox.Item>
<ListBox.Item key="banana">Banana</ListBox.Item>
</ListBox><Form onSubmit={handleSubmit}>
<ListBox
name="technology"
label="Preferred Technology"
isRequired
selectionMode="single"
>
<ListBox.Section title="Frontend">
<ListBox.Item key="react">React</ListBox.Item>
<ListBox.Item key="vue">Vue.js</ListBox.Item>
</ListBox.Section>
</ListBox>
<Form.Submit>Submit</Form.Submit>
</Form>Tab- Moves focus to/from the ListBoxArrow Keys- Navigate between optionsSpace/Enter- Select/deselect the focused optionHome/End- Move to first/last optionPage Up/Page Down- Move up/down by multiple itemsEscape- Deselect all items (if onEscape not provided)
- ListBox announces as "listbox" with proper role
- Selected items are announced as "selected"
- Section headings are properly associated with their items
- Selection changes are announced immediately
- Item descriptions are read along with labels
aria-label- Provides accessible label when no visible label existsaria-labelledby- Associates with external label elementaria-describedby- Associates with description textaria-multiselectable- Indicates multiple selection capabilityaria-activedescendant- Tracks focused item for screen readers
-
Do: Provide clear, descriptive labels for options
<ListBox.Item key="react" description="JavaScript library for UIs"> React </ListBox.Item>
-
Don't: Use ListBox for very large datasets without considering virtualization
// ✅ Virtualization is automatic for lists without sections <ListBox height="300px"> {hugeArray.map(item => <ListBox.Item key={item.id}>{item.name}</ListBox.Item>)} </ListBox>
-
Do: Use sections to organize related options
<ListBox.Section title="Frontend Frameworks"> <ListBox.Item key="react">React</ListBox.Item> </ListBox.Section>
-
Do: Use
isCheckablefor clearer multiple selection UI<ListBox selectionMode="multiple" isCheckable={true}> {/* Checkboxes make the selection state obvious */} </ListBox>
-
Do: Use
showSelectAllfor efficient multiple selection from lists<ListBox selectionMode="multiple" isCheckable={true} showSelectAll selectAllLabel="Select All"> {/* Easy bulk selection with visual feedback */} </ListBox>
-
Do: Use
textValuefor complex option content<ListBox.Item key="item" textValue="Basic Plan - Free"> <Space gap="1x" flow="column"> <Text weight="600">Basic Plan</Text> <Badge>Free</Badge> </Space> </ListBox.Item>
-
Do: Leverage Item features for rich content
<ListBox.Item key="admin" icon={<UserIcon />} description="Full access" suffix={<Badge>3</Badge>} hotkeys="ctrl+a" > Administrator </ListBox.Item>
-
Accessibility: Always provide meaningful labels and descriptions
-
Performance: Virtualization is automatic when there are no sections
-
UX: Consider FilterListBox for searchable lists with many options
This component supports all Field properties when used within a Form. The component automatically handles form validation, field states, and integrates with form submission.
<Form onSubmit={handleSubmit}>
<ListBox
name="preferences"
label="User Preferences"
isRequired
rules={[{ required: true }]}
selectionMode="multiple"
>
<ListBox.Item key="notifications">Email Notifications</ListBox.Item>
<ListBox.Item key="newsletter">Newsletter</ListBox.Item>
</ListBox>
</Form>For large datasets, use the items prop with a render function. This enables automatic virtualization — only visible items are rendered in the DOM, providing smooth scrolling even with thousands of items.
const items = Array.from({ length: 1000 }, (_, i) => ({
key: `item-${i}`,
label: `Item ${i + 1}`,
}));
<ListBox label="Large Dataset" selectionMode="single" items={items}>
{(item) => <ListBox.Item key={item.key}>{item.label}</ListBox.Item>}
</ListBox>Note: Virtualization is disabled when sections are present. For very large datasets, prefer a flat list structure.
- Use
textValueprop for complex option content to improve accessibility and search - Avoid sections for very large lists to enable virtualization
- Use
itemsprop pattern with dynamic data for better performance - Consider FilterListBox for searchable large lists (100+ items)
- Virtualization handles items with varying heights automatically
- FilterListBox - ListBox with integrated search functionality
- FilterPicker - ListBox in a trigger-based popover
- Select - Dropdown selection without persistent visibility
- ComboBox - Dropdown with text input and search
- RadioGroup - Single selection with radio buttons
- CheckboxGroup - Multiple selection with checkboxes