Skip to content

Latest commit

 

History

History
798 lines (630 loc) · 25.3 KB

File metadata and controls

798 lines (630 loc) · 25.3 KB

import { Meta, Canvas, Story } from '@storybook/addon-docs/blocks'; import { ListBox } from './ListBox'; import * as ListBoxStories from './ListBox.stories';

ListBox

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.

When to Use

  • 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)

Component


Properties

  • selectedKey string — The selected key in controlled mode
  • defaultSelectedKey string — The default selected key in uncontrolled mode
  • selectedKeys string[] | 'all' — The selected keys in controlled multiple mode. Use "all" to select all items or an array of keys.
  • defaultSelectedKeys string[] | '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 mode
  • disallowEmptySelection boolean (default: false) — Whether to disallow empty selection
  • disabledKeys Key[] — Array of keys for disabled items
  • size 'small' | 'medium' | 'large' (default: medium) — ListBox size
  • shape 'card' | 'plain' | 'popover' (default: card) — Visual shape of the ListBox
  • filter (nodes: Iterable<Node>) => Iterable<Node> — Filter function for the list items
  • emptyLabel ReactNode (default: No items) — Label shown when no items are available
  • header ReactNode — Custom header content
  • footer ReactNode — Custom footer content
  • focusOnHover boolean (default: true) — Whether moving the pointer over an option will move DOM focus to that option
  • shouldUseVirtualFocus boolean (default: false) — Whether to use virtual focus instead of DOM focus
  • isCheckable boolean (default: false) — Whether to show checkboxes for multiple selection mode
  • shouldFocusWrap boolean (default: false) — Whether keyboard navigation should wrap around
  • description string — Field description
  • message string — Help or error message
  • showSelectAll boolean (default: false) — Whether to show the "Select All" option in multiple selection mode
  • selectAllLabel string (default: Select All) — Label for the "Select All" option
  • onSelectionChange (keys: Key | Key[] | 'all' | null) => void — Callback when selection changes
  • onEscape () => void — Callback when Escape key is pressed
  • onOptionClick (key: Key) => void — Callback when an option is clicked (non-checkbox area)
  • items Iterable<T> — Array of items for dynamic content with render function pattern
  • disableSelectionToggle boolean (default: false) — When true, clicking an already-selected item keeps it selected instead of toggling it off

Base Properties

Supports Base properties

Field Properties

Supports all Field properties

Styling Properties

styles

Customizes the root wrapper element of the component.

Sub-elements:

  • ValidationState - Container for validation and loading indicators

listStyles

Customizes the list container element.

optionStyles

Customizes individual option elements.

Sub-elements:

  • Label - The main text of each option
  • Description - Secondary descriptive text for options
  • Content - Container for label and description
  • Checkbox - Checkbox element when isCheckable={true}
  • CheckboxWrapper - Wrapper around the checkbox

sectionStyles

Customizes section wrapper elements.

headingStyles

Customizes section heading elements.

headerStyles

Customizes the header area when header prop is provided.

footerStyles

Customizes the footer area when footer prop is provided.

Style Properties

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

Modifiers

The mods property accepts the following modifiers you can override:

  • invalid boolean — Applied when validationState="invalid"
  • valid boolean — Applied when validationState="valid"
  • disabled boolean — Applied when isDisabled={true}
  • focused boolean — Applied when the ListBox has focus
  • header boolean — Applied when header prop is provided or showSelectAll={true}
  • footer boolean — Applied when footer prop is provided
  • selectAll boolean — Applied when showSelectAll={true} in multiple selection mode

Key Properties

Selection Behavior

isCheckable (boolean, default: false)

  • When true in 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 true in 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

Visual Styling

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 use
  • popover: No border, but keeps margin and radius - suitable for overlay use
  • Use plain when embedding ListBox directly into another component
  • Use popover when using ListBox inside overlays (Dialog, ComboBox, Picker)

Focus and Interaction

focusOnHover (boolean, default: true)

  • When true, moving the pointer over an option will move DOM focus to that option
  • Set to false for 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

Event Handlers

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

Advanced

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

Sub-components

ListBox.Item

Individual items within the ListBox. Each item is rendered using Item and supports all Item properties for layout, icons, descriptions, and interactive features.

Item API

ListBox.Item is built using Item, which provides rich layout and interaction capabilities. All Item properties are supported:

  • key string \| number — Unique identifier for the item (required)
  • children ReactNode — The main content/label for the option
  • textValue string — Text representation for complex JSX content (for screen readers and search)
  • icon ReactNode — Icon displayed before the content
  • rightIcon ReactNode — Icon displayed after the content
  • description ReactNode — Secondary text below the main content
  • descriptionPlacement 'inline' \| 'block' (default: 'block') — How the description is positioned
  • prefix ReactNode — Content displayed at the start (before icon)
  • suffix ReactNode — Content displayed at the end (after content)
  • hotkeys string — Keyboard shortcut hint (e.g., 'ctrl+a') displayed as suffix
  • tooltip string \| object — Tooltip shown on hover. Object: { title, description, placement }
  • styles Styles — Custom styling for the item
  • qa string — QA identifier for testing

Example with Rich Items

<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>

ListBox.Section

Groups related items together with an optional heading.

  • title ReactNode — Optional heading text for the section
  • children ListBox.Item[] — Collection of ListBox.Item components

Variants

Selection Modes

  • single - Allows selecting only one item at a time
  • multiple - Allows selecting multiple items
  • none - No selection allowed (display only)

Sizes

  • 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)

Examples

Basic Usage

<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>

With Descriptions

<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>

With Sections

<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>

With Sections (Dynamic)

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>

Multiple Selection

<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>

Multiple Selection with Checkboxes

<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>

Multiple Selection with Select All

<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>

With Header and Footer

<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>

Controlled Selection

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>

Disabled Items

<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>

Disallow Empty Selection

<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>

Rich Content Options

<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>

With Tooltips

<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>

With Complex Content

<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>

Different Sizes

<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>

Visual Shape Variants

{/* 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>

Custom Escape Handling

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>

In Forms

<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>

Accessibility

Keyboard Navigation

  • Tab - Moves focus to/from the ListBox
  • Arrow Keys - Navigate between options
  • Space/Enter - Select/deselect the focused option
  • Home/End - Move to first/last option
  • Page Up/Page Down - Move up/down by multiple items
  • Escape - Deselect all items (if onEscape not provided)

Screen Reader Support

  • 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 Properties

  • aria-label - Provides accessible label when no visible label exists
  • aria-labelledby - Associates with external label element
  • aria-describedby - Associates with description text
  • aria-multiselectable - Indicates multiple selection capability
  • aria-activedescendant - Tracks focused item for screen readers

Best Practices

  1. Do: Provide clear, descriptive labels for options

    <ListBox.Item key="react" description="JavaScript library for UIs">
      React
    </ListBox.Item>
  2. 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>
  3. Do: Use sections to organize related options

    <ListBox.Section title="Frontend Frameworks">
      <ListBox.Item key="react">React</ListBox.Item>
    </ListBox.Section>
  4. Do: Use isCheckable for clearer multiple selection UI

    <ListBox selectionMode="multiple" isCheckable={true}>
      {/* Checkboxes make the selection state obvious */}
    </ListBox>
  5. Do: Use showSelectAll for efficient multiple selection from lists

    <ListBox selectionMode="multiple" isCheckable={true} showSelectAll selectAllLabel="Select All">
      {/* Easy bulk selection with visual feedback */}
    </ListBox>
  6. Do: Use textValue for 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>
  7. 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>
  8. Accessibility: Always provide meaningful labels and descriptions

  9. Performance: Virtualization is automatic when there are no sections

  10. UX: Consider FilterListBox for searchable lists with many options

Integration with Forms

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>

Performance

Virtualization

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.

Optimization Tips

  • Use textValue prop for complex option content to improve accessibility and search
  • Avoid sections for very large lists to enable virtualization
  • Use items prop pattern with dynamic data for better performance
  • Consider FilterListBox for searchable large lists (100+ items)
  • Virtualization handles items with varying heights automatically

Related Components

  • 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