build69
build69  /  Components  /  Page Structure  /  BulkActionBar

BulkActionBar

v1.0.2Updated 19 Oct 2026

The 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
DefaultOpen ↗
▸Why this is recommendedbulkselectionselect allbatch+5

The default, out-of-the-box BulkActionBar.

Recommended for+14
  • ✓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
Tags+6
bulkselectionselect allbatchactionslisttabletoolbardefault
Features+4
  • 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
Not recommended for−2
  • ✕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.

get_registry_item({ name: "BulkActionBar" })

Usage

import { BulkActionBar } from '@/components/patterns/bulk-action-bar';

<BulkActionBar
  selectedCount={selected.size}
  totalCount={rows.length}
  onSelectAll={() => setSelected(new Set(rows.map((r) => r.id)))}
  onDeselectAll={() => setSelected(new Set())}
  actions={[
    { id: 'export', label: 'Export', onRun: () => api.export([...selected]) },
    { id: 'open', label: 'Open', onRun: open, enabledFor: (n) => n === 1,
      disabledReason: 'Open takes one row at a time' },
    { id: 'delete', label: 'Delete', tone: 'danger', onRun: () => api.remove([...selected]) },
  ]}
/>

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

PropTypeRequired
selectedCount

How many rows are ticked. The bar’s whole reason to exist; 0 renders nothing unless hideWhenEmpty is false

numberyes
totalCount

How 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

number—
onSelectAll

Tick everything. Omit and no select-all checkbox is rendered

() => void—
onDeselectAll

Untick 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

() => void—
actions

BulkAction = { id, label, icon?, tone?, onRun, enabledFor?, disabledReason?, disabled? }. onRun may return a Promise, which drives the running state

BulkAction[]—
quickSelections

BulkQuickSelection = { id, label, count?, onSelect, disabled? } — extra ways to select, behind a menu

BulkQuickSelection[]—
runningActionId

Controlled running state. When supplied the bar stops tracking its own, so a host that already knows a job is in flight owns it

string | null—
hideWhenEmpty

Render nothing while nothing is selected (default true). false keeps the bar in the layout so the list does not jump

boolean—
size

Control size (default "xs", which is what a dense list wants)

'xs' | 'sm'—
labels

Override 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

Partial<BulkActionBarLabels>—

Related