BulkActionBar
v1.0.2Updated 19 Oct 2026The bar a list shows once rows are ticked: a three-state select-all, the selected count, optional quick selections behind a menu, the actions, and Deselect all. Lifted out because every list was rebuilding it from a Checkbox and some Buttons and each rebuild got a different part wrong. An action declares which selection SIZES it applies to and why it does not apply otherwise, and the reason reaches both the tooltip and the accessible name. While one action runs the bar stays put, that action shows it is working and the rest disable, so a second bulk delete cannot start on rows the first one is deleting.
Preview
Live · 7 stories▸Why this is recommendedbulkselectionselect allbatch+5
The default, out-of-the-box BulkActionBar.
- ✓Let people pick several rows and act on all of them
- ✓A select-all checkbox that knows the difference between some and all
- ✓Bulk delete / archive / export from a list or a table
- ✓Show why a bulk action is unavailable for the current selection
- Three-state select-all — none / indeterminate / all; an indeterminate box SELECTS the rest rather than clearing the selection
- Never claims "all" without a totalCount, because it cannot compare a page of ten against a thousand
- Count readout through the locale formatter (Thai numerals under a LocaleProvider), with or without a total
- Per-action enabledFor(selectedCount) — the action that only works on one row sits beside the ones that take many
- A disabled action carries its reason in the title AND the accessible name, so a keyboard user is told rather than shown grey
- Running state the bar tracks itself from a Promise returned by onRun (aria-busy, a spinner, and the running action named in its accessible name), or controlled via runningActionId
- A rejected action releases the bar exactly as a resolved one does — a failed bulk job must not leave the list unusable
- Quick selections behind a menu ("everything on this page", "everything matching the filter"), each with an optional count
- hideWhenEmpty (default true) or keep the space so the list does not jump when the first row is ticked
- Labelled role="region" landmark; every control reachable by keyboard in reading order, disabled ones skipped
- i18n: labels prop over DEFAULT_BULK_ACTION_BAR_LABELS — the count templates, the select-all names, the quick-select trigger, the fallback unavailable reason and the running announcement
- ✕A single row’s actions (use a DropdownMenu on the row)
- ✕Unsaved-changes save/discard (use ContextualSaveBar)
- ✕Filtering a list (use FilterBar)
- ✕Progress of one long server job (use the job panel pattern)
Install
Ask your agent, or call the MCP tool directly. The resolved bundle is written into your project.
Usage
Tick variants in the left menu — each adds a tab with that variant’s real story source, the same code the preview above renders.
Props
selectedCountHow many rows are ticked. The bar’s whole reason to exist; 0 renders nothing unless hideWhenEmpty is false
yestotalCountHow many rows there are in total. Without it the readout omits the total and the select-all box never reaches the checked state — claiming "all" from a count it cannot compare is how a bar tells a user everything is ticked when a page of ten is
—onSelectAllTick everything. Omit and no select-all checkbox is rendered
—onDeselectAllUntick everything. Both the "Deselect all" button and the dismiss ✕ call this and nothing else — wiring the ✕ to the select-all toggle is the bug this component replaces
—actionsBulkAction = { id, label, icon?, tone?, onRun, enabledFor?, disabledReason?, disabled? }. onRun may return a Promise, which drives the running state
—quickSelectionsBulkQuickSelection = { id, label, count?, onSelect, disabled? } — extra ways to select, behind a menu
—runningActionIdControlled running state. When supplied the bar stops tracking its own, so a host that already knows a job is in flight owns it
—hideWhenEmptyRender nothing while nothing is selected (default true). false keeps the bar in the layout so the list does not jump
—sizeControl size (default "xs", which is what a dense list wants)
—labelsOverride any built-in string. Keys: bulkActions, selectedCount ({count}), selectedOfTotal ({count}/{total}), selectAll, deselectAll, deselectAllAction, quickSelect, unavailableForSelection, clearSelection, runningAction ({action}). English defaults ship verbatim as DEFAULT_BULK_ACTION_BAR_LABELS; numbers format through LocaleProvider, never by hand
—