Persona

List Box

Overlay · 9 props · Updated Sep 22, 2026

A list box shows every option at once inside a bordered box, and can manage the list as well as present it: rows can be added, deleted and dragged into a new order without leaving the control. ListBoxField is the labeled form of it, and the one place among the composed fields where the wrapper changes what the control does rather than only what surrounds it.

  • Passport
    Driver’s licence
    National ID card
    Passport
    Driver’s licence
    National ID card
    Container the bordered surface holding every item at once, scrolling past maxHeight rather than collapsing into a menu.
  • Passport
    Driver’s licence
    National ID card
    Passport
    Driver’s licence
    National ID card
    List item one entry. It can carry its own actions menu and delete control.
  • Passport
    Driver’s licence
    National ID card
    Passport
    Driver’s licence
    National ID card
    Add button (optional) drawn from onAdd, below the list rather than inside it.
  • Passport
    Driver’s licence
    National ID card
    Passport
    Driver’s licence
    National ID card
    Delete button (optional) drawn from onDelete, and disabled while no selectedKeys are passed at all — which is why the specimen has a selected row.

Use a list box when the set of options is the information — an allowlist, a set of accepted document types, the fields chosen for an export. Seeing all of them together is the point, which is also the constraint: the box is 204px wide unless you set fullWidth, and maxHeight caps its height and scrolls inside the border. A list long enough to need that scroll has given up the one thing a list box offers over a menu.

Use a Select when the list is only a way to reach one value, and a Combobox when it is long enough that the reader would rather type than look. Use Checkboxes for a handful of fixed options that nobody adds to — a list box spends a border, a footer and 204px on machinery those options never use.

Section titled “ListBoxField moves Add and Delete out of the footer”

ListBoxField is a list box in a FormField, so it brings the label, the helper text and the validation messages. It also takes onAdd and onDelete away from the list box and rebuilds them beside the label: Delete becomes the first item of an overflow menu, alongside anything passed in actions, and Add becomes a plus icon button, or a dropdown of options when given addDropdownConfig.

Those controls only render in config-field mode. configField is what draws them, and it also sets fullWidth and drops the rows to sm. Outside it — a ListBoxField standing alone, with no configField and no enclosing config form group — onAdd, onDelete, actions, addDropdownConfig and addDisabled are all accepted and none of them renders anything, because they never reach the list box either. The footer buttons documented under Editing belong to ListBox on its own.

So: a list box with a label inside a config form is a ListBoxField. A list box that needs its footer is a ListBox, wrapped in a hand-composed FormField if it needs a label with it.

ListBoxField also holds the selection itself, which ListBox does not: it seeds state from defaultSelectedKeys, calls your onSelectionChange and then stores the keys, so the menu can act on them. selectedKeys still overrides it.

Set a selection mode before relying on the selection

Section titled “Set a selection mode before relying on the selection”

Selection comes from React Aria’s GridList, which the props table records at the bottom as the set of props this component also accepts. selectionMode is one of them, and it governs whether rows can be selected at all: none, single or multiple. Leave it out and rows are not selectable, which makes Delete a button that receives nothing.

Selection is also controlled, not remembered. selectedKeys and onSelectionChange are yours to hold, and onDelete is handed whatever selectedKeys currently is — plural, because the API assumes more than one row can be acted on at once.

Disable Delete yourself when nothing is selected

Section titled “Disable Delete yourself when nothing is selected”

deleteDisabled is the only switch that does it. The component disables Delete when selectedKeys is absent entirely, which covers the uncontrolled case and nothing else: an empty Set is a present value, so a controlled list with no rows selected renders Delete enabled and calls onDelete with an empty selection. Derive deleteDisabled from the size of your own selection.

One more thing moves the selection without the reader doing it: while a row’s dropdown is open, a mousedown anywhere outside the box clears the selection through onSelectionChange. That is deliberate: it closes the row’s popover by deselecting the row the popover belongs to. It does mean selection is not something you can treat as sticky.

Section titled “Draw a footer button by supplying its handler”

onAdd draws Add, onDelete draws Delete, and neither appears without its handler. So a list with no handlers has no footer, and that is how a read-only list is built.

editable gates the same pair, and both conditions have to hold. It defaults to true, which makes it a veto rather than a switch: it can hide buttons that exist, and it cannot summon buttons that do not. Passing editable on its own shows nothing.

Omit the handlers for a genuinely read-only list

Section titled “Omit the handlers for a genuinely read-only list”

editable={false} does not make a list read-only. With onAdd still supplied, the empty state remains a button and still calls onAdd — the footer disappears, the behavior does not. A per-row delete is a separate prop again: onDelete on ListBox.Item draws an X on the row and editable does not reach it.

Pass items as well as reorderConfig for drag and drop

Section titled “Pass items as well as reorderConfig for drag and drop”

reorderConfig enables dragging. It takes onReorder, which receives the whole reordered array to replace your state with, and itemKeyPath, the property name that produced each row’s key. It needs the items prop too. Supply reorderConfig while rendering rows as children without items and the drag handles appear, the drag works, and the drop silently does nothing, because the reorder handler returns early when it has no array to reorder.

A row can be edited in place. dropdownFormFields or dropdownContent on ListBox.Item open a popover when the row is selected, with dropdownHeadingText and dropdownFooter framing it; actions gives the row its own menu instead. What a list box cannot do is edit a row inline in the list — the row is a label, and everything else happens in the surface above it.

The name is the widget, not the markup. This component is built on React Aria’s GridList, so measured on this page’s own specimen the container is role="grid", each row is role="row", and each row’s content sits in a role="gridcell". There is no role="listbox" and no role="option" anywhere in it.

That is the right choice for rows that carry their own buttons — a listbox option cannot hold an interactive control, and these rows hold a delete icon and an actions menu. It does mean any guidance written against the ARIA list box pattern does not describe this component, and that aria-selected is set on rows by React Aria rather than by you.

The grid takes a single tab stop and moves between rows with the arrow keys, so a list of forty rows costs the keyboard one Tab. Give the container an aria-label, since the border is the only thing naming it on screen.

A ListBoxField does not remove that obligation. Its label is visible text above a role="grid" element, and a for attribute cannot name one, so the grid is still unnamed until you pass aria-label as well.

Write the empty text for the empty state you actually have

Section titled “Write the empty text for the empty state you actually have”

emptyText fills two different blanks. With onAdd supplied the empty state is a button, and emptyText is its label — write an action, and PDS falls back to “Click to add an option”. Without onAdd it is a message, and PDS falls back to “No options.” Those defaults are correct for their cases and a single hand-written string will not be: “No document types yet” in a list you can add to leaves the reader looking at a button that reads like a status.

Rows are one line, clipped, with no secondary text, so two rows that read the same are two rows nobody can tell apart — including the reader who selects one to delete. Where the rows are user-generated, warn before accepting a duplicate rather than letting the list hold two.

Capitalization and the rules that apply to every label are on Writing.

ListBoxField first. Its table lists onAdd, onDelete, actions, addDropdownConfig and addDisabled with nothing to say that all five render only in config-field mode; the section above is that note. editable is in the table too and is moot here, since the buttons it vetoes are never drawn.

NameType
actions

ListBoxFieldAction[]

addDropdownConfig

AddDropdownConfig

addDisabled

boolean

Disables the add button, e.g. while the list is read-only.

label

ReactNode

Renders <Label> above input

inputId

string

ID of the input element. Providing this will automatically link the label to the input.

helperText

ReactNode

Renders <HelperText> below input

errorText

ReactNode

Renders <ErrorText> below input and helper text.

warningText

ReactNode

Renders <WarningText> below input and helper text.

labelProps

FormLabelProps

helperProps

(HelperTextProps & RefAttributes<HTMLDivElement>)

warningProps

(Omit<DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>, “ref”> & RefAttributes<HTMLDivElement>)

errorProps

(Omit<DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>, “ref”> & RefAttributes<HTMLDivElement>)

configField

boolean

Whether to render the form field as a config field. This will render the helper text as a tooltip.

configFieldInputWidth

string

Override the width of the input area when configField is true. Accepts any valid CSS width value (e.g. ‘33%’, ‘66.67%’). Defaults to ‘50%‘.

rightActions

ReactNode

Renders actions to the right of the label.

onAdd

MaybePromise

Called when “Add” button is clicked. Only renders “Add” button if this prop is present.

onDelete

((selectedKeys: “all” | Iterable<Key> | undefined) => void)

Called when “Delete” button is clicked. Only renders “Delete” button if this prop is present.

deleteDisabled

boolean

Whether or not delete button should be disabled.

editable

boolean

Whether or not the Add/Delete buttons should be shown.

emptyText

string

Text to display if no options are available.

fullWidth

boolean

Whether or not to expand the box to the full width of the container.

reorderConfig

{ onReorder: (reorderedItems: T[]) => void; itemKeyPath: string; }

When provided, enables drag and drop functionality.

size

sm, md

Size of the list box.

maxHeight

number, string

Caps the list height and scrolls vertically inside the bordered container.

Also accepts every prop of Omit< ComponentPropsWithoutRef<typeof GridList<T>>, ‘slot’ | ‘autoFocus’ | ‘orientation’ | ‘shouldFocusWrap’ | ‘dragAndDropHooks’ | ‘layout’ >.

List Box’s own props:

NameType
onAdd

MaybePromise

Called when “Add” button is clicked. Only renders “Add” button if this prop is present.

onDelete

((selectedKeys: “all” | Iterable<Key> | undefined) => void)

Called when “Delete” button is clicked. Only renders “Delete” button if this prop is present.

deleteDisabled

boolean

Whether or not delete button should be disabled.

editable

boolean

Whether or not the Add/Delete buttons should be shown.

emptyText

string

Text to display if no options are available.

fullWidth

boolean

Whether or not to expand the box to the full width of the container.

reorderConfig

{ onReorder: (reorderedItems: T[]) => void; itemKeyPath: string; }

When provided, enables drag and drop functionality.

size

sm, md

Size of the list box.

maxHeight

number, string

Caps the list height and scrolls vertically inside the bordered container.

Also accepts every prop of Omit< ComponentPropsWithoutRef<typeof GridList<T>>, ‘slot’ | ‘autoFocus’ | ‘orientation’ | ‘shouldFocusWrap’ | ‘dragAndDropHooks’ | ‘layout’ >.

Was this page helpful?