Patterns
Bulk actions
Selecting several rows and acting on them at once; when the bar shows, how the count is announced and how deleting is confirmed.
A bulk action lets users apply one action to many rows in one step. This pattern fixes the selection, the action bar and how the result is announced.
When to use
Section titled “When to use”- When users need to apply the same action to many rows of a table or list.
- When the actions make sense for all selected items: archive, export, delete.
- For single-row actions use row actions; no bar is needed.
Live example
Section titled “Live example”Select rows. The bar appears with the first selection and shows the count. “Archive” clears the selection and reports the result in a toast with an “Undo” action. “Delete” first opens a confirmation dialog. Escape in the bar clears the selection.
| Status | |||
|---|---|---|---|
| Item 1 | 50 | ||
| Item 2 | 100 | ||
| Item 3 | 150 | ||
| Item 4 | 200 | ||
| Item 5 | 250 | ||
| Item 6 | 300 | ||
| Item 7 | 350 | ||
| Item 8 | 400 |
import { ActionBar, AlertDialog, createDataTableColumnHelper, DataTable, Link, Stack, StatusBadge, Text, ToolbarButton, useToast, type DataTableRowSelection } from '@bigbang/ui';import { useMemo, useState } from 'react';import { type Lang, LangProvider } from '../lang';
const copy = { tr: { caption: 'Öğe listesi', name: 'Ad', status: 'Durum', count: 'Adet', item: 'Öğe', archive: 'Arşivle', export: 'Dışa aktar', delete: 'Sil', selected: (count: number) => `${count} öğe seçildi`, archived: (count: number) => `${count} öğe arşivlendi`, exported: (count: number) => `${count} öğe dışa aktarıldı`, deleted: (count: number) => `${count} öğe silindi`, undo: 'Geri al', confirmTitle: (count: number) => `${count} öğe silinsin mi?`, confirmText: 'Seçili öğeler listeden kaldırılır. Bu işlem geri alınamaz.', confirmLabel: (count: number) => `${count} öğeyi sil`, cancel: 'Vazgeç', rule: (count: number) => `Silme, sayıyı düğmede gösteren bir onay ister: “${count} öğeyi sil”.`, ruleLink: 'Onay kalıbı', confirmHref: '/patterns/confirmation/', }, en: { caption: 'Item list', name: 'Name', status: 'Status', count: 'Count', item: 'Item', archive: 'Archive', export: 'Export', delete: 'Delete', selected: (count: number) => `${count} selected`, archived: (count: number) => `${count} ${count === 1 ? 'item' : 'items'} archived`, exported: (count: number) => `${count} ${count === 1 ? 'item' : 'items'} exported`, deleted: (count: number) => `${count} ${count === 1 ? 'item' : 'items'} deleted`, undo: 'Undo', confirmTitle: (count: number) => `Delete ${count} ${count === 1 ? 'item' : 'items'}?`, confirmText: 'The selected items are removed from the list. This cannot be undone.', confirmLabel: (count: number) => `Delete ${count} ${count === 1 ? 'item' : 'items'}`, cancel: 'Cancel', rule: (count: number) => `Deleting asks for a confirmation that shows the count on the button: “Delete ${count} ${count === 1 ? 'item' : 'items'}”.`, ruleLink: 'Confirmation pattern', confirmHref: '/en/patterns/confirmation/', },};
type Row = { id: string; name: string; status: 'active' | 'pending' | 'draft' | 'archived'; count: number };const statusOrder = ['active', 'pending', 'draft'] as const;const helper = createDataTableColumnHelper<Row>();
/** * A table with row selection and an action bar. The bar appears with the first selected row and says how many are selected. * An action runs on all selected rows, clears the selection and reports the result in a toast; archive offers Undo. * Delete asks for confirmation first, with the count in the dialog title and on its button. */export function BulkActionsDemo({ lang = 'tr' }: { lang?: Lang }) { return ( <LangProvider lang={lang}> <BulkTable lang={lang} /> </LangProvider> );}
function BulkTable({ lang }: { lang: Lang }) { const t = copy[lang]; const toast = useToast(); const [rows, setRows] = useState<Row[]>(() => Array.from({ length: 8 }, (_, index) => ({ id: `row-${index + 1}`, name: `${t.item} ${index + 1}`, status: statusOrder[index % statusOrder.length] ?? 'draft', count: (index + 1) * 50 })), ); const [selection, setSelection] = useState<DataTableRowSelection>({}); const [confirmOpen, setConfirmOpen] = useState(false); const count = Object.keys(selection).length;
const columns = useMemo( () => [ helper.accessor('name', { header: t.name, meta: { rowHeader: true } }), helper.accessor('status', { header: t.status, enableSorting: false, cell: (context) => <StatusBadge status={context.getValue()} /> }), helper.accessor('count', { header: t.count, meta: { numeric: true } }), ], [t], );
const archive = () => { const previous = new Map(rows.filter((row) => selection[row.id]).map((row) => [row.id, row.status])); setRows((current) => current.map((row) => (previous.has(row.id) ? { ...row, status: 'archived' } : row))); toast.show({ title: t.archived(count), tone: 'success', action: { label: t.undo, onClick: () => setRows((current) => current.map((row) => (previous.has(row.id) ? { ...row, status: previous.get(row.id) ?? row.status } : row))) }, }); setSelection({}); }; const exportRows = () => { toast.show({ title: t.exported(count), tone: 'success' }); setSelection({}); }; const deleteRows = () => { setRows((current) => current.filter((row) => !selection[row.id])); toast.show({ title: t.deleted(count), tone: 'success' }); setSelection({}); };
return ( <> <Stack gap="md"> <ActionBar placement="inline" count={count} countLabel={t.selected} onClear={() => setSelection({})}> <ToolbarButton label={t.archive} icon="file" onClick={archive} /> <ToolbarButton label={t.export} icon="download" onClick={exportRows} /> <ToolbarButton label={t.delete} icon="trash" onClick={() => setConfirmOpen(true)} /> </ActionBar> <DataTable<Row> columns={columns} data={rows} getRowId={(row) => row.id} getRowLabel={(row) => row.name} caption={t.caption} captionHidden selectionMode="multiple" rowSelection={selection} onRowSelectionChange={setSelection} // The action bar announces the count; the table stays quiet so it is read once. messages={{ 'selected-count': '' }} /> <Text size="sm" tone="subtle"> {t.rule(Math.max(count, 3))} <Link href={t.confirmHref}>{t.ruleLink}</Link> </Text> </Stack> <AlertDialog open={confirmOpen} onOpenChange={setConfirmOpen} title={t.confirmTitle(count)} description={t.confirmText} confirmLabel={t.confirmLabel(count)} cancelLabel={t.cancel} tone="danger" onConfirm={deleteRows} /> </> );}| Rule | Value |
|---|---|
| Bar | Visible only while at least 1 row is selected. |
| Placement | placement="floating" (fixed to the bottom of the viewport) on long pages; placement="inline" when the table has a fixed area above it. The example uses inline. |
| Number of actions | 3 to 5. The most used action first, the destructive one last. |
| Count | “3 selected” in the bar; announced once to screen readers. |
| Escape | Clears the selection from inside the bar. |
| Header checkbox | Selects the rows of the visible page. |
| All pages | A “Select all 1,000 items” link is offered; it stays apart from the page selection. |
| After an action | The selection clears; the result is reported in a Toast: “3 items archived”. A reversible action offers “Undo” in the toast. |
| Destructive action | Asks for confirmation in an AlertDialog; the count is in the title and the button: “Delete 3 items?”, “Delete 3 items”. See Confirmation. |
3 items archived
Action completed
Behaviour
Section titled “Behaviour”- The user selects a row or the header checkbox. The header checkbox selects only the visible page.
- The bar appears and shows the selected count. With several pages, a “Select all N items” link is offered.
- The user chooses an action. It applies to all selected rows.
- When the action finishes, the selection clears, the bar closes and a toast reports the result. For a reversible action such as archive, the toast offers “Undo”.
- Deleting asks for confirmation first: a danger
AlertDialogopens with the count in the title and on the button. Focus starts on “Cancel”; if the user cancels, the rows and the selection stay. The dialog itself is defined in the Confirmation pattern.
Accessibility
Section titled “Accessibility”- The bar is a toolbar named “Bulk actions”; arrow keys move between the actions.
- The selected count is read once, politely, from a status region. The table’s own selection announcement is turned off in the example, so the count is not read twice.
- “Clear selection” is the first button and does the same as Escape.
- Every row checkbox names its row: “Select row Item 1”.
- The action result is announced politely in the “Notifications” region without moving focus (WCAG 4.1.3). Undo is a button inside the toast.
- The confirmation dialog has the
alertdialogrole and does not close on an outside click.
Content
Section titled “Content”- Action names are verbs: “Archive”, “Export”, “Delete”.
- The result gives the count and the verb: “3 items archived”.
- The destructive confirm button carries the count: “Delete 3 items”.
- The all-pages link gives the total: “Select all 1,000 items”.
Motion
Section titled “Motion”| Item | Motion | Preset |
|---|---|---|
| Bar appearing | 240 ms, 8 px rise and fade | motion.toast |
| Count change | 110 ms cross-fade; no counting animation | motion.reveal |
| Action buttons | 0.97 scale on press | motion.press |
With reduced motion, the shift is removed and only the fade stays.
Components used
Section titled “Components used”DataTable, ActionBar, Toolbar, Toast, AlertDialog, StatusBadge, Link, Button. Related patterns: Confirmation, Filtering, Data export.