Bulk Action Bar
- Stable
- WCAG 2.2 evidence
- RTL
The bar that appears over a table while rows are selected: a checkbox and the selection count, an
optional "Select all", the bulk actions, and a button that clears the selection. Render it only while at least one row is
selected. It is a React Aria Toolbar (role="toolbar"): Tab enters it once and the arrow keys move between its controls, mirrored in
right-to-left layouts (WCAG 2.1.1, 4.1.2). The count is announced through React Aria's polite live region when the bar appears
and whenever the text changes, so screen reader users learn about the selection without moving focus (4.1.3); the visible
text is not itself a live region, so it is not read twice. The checkbox is named by the count and shows the mixed or checked
state with a dash or a tick, not colour (1.4.1); every button keeps its own focus ring (2.4.7) and 32 px height (2.5.8). Consumer duties: the wording of the count and actions, confirming destructive actions, moving focus sensibly after an action
removes the selected rows, and scroll-padding on a scroller when the bar is fixed over it (2.4.11).
import { BulkActionBar } from "@nexera-ui/react";- 6
- examples
- 10
- props
- 2
- live controls
- 1
- platform
- 11
- WCAG criteria
- 1
- block uses it
Try every prop. Copy the code.
Change the props and the code updates. Check light and dark, LTR and RTL, and phone width.
import { BulkActionBar } from "@nexera-ui/react";
<BulkActionBar />Examples 5
The same examples as Storybook, rendered live. Open Code to copy one.
All selected
Every row selected (isAllSelected): the checkbox shows a tick and "Select all" is not needed.
import { BulkActionBar, Button } from "@nexera-ui/react";
import { LuDownload, LuMessageSquare, LuTrash2, LuUsers } from "react-icons/lu";
const actions = (
<>
<Button variant="secondary" leadingIcon={<LuMessageSquare />}>
Message
</Button>
<Button variant="secondary" leadingIcon={<LuDownload />}>
Export
</Button>
<Button variant="secondary" leadingIcon={<LuUsers />}>
Change team
</Button>
<Button variant="destructive" leadingIcon={<LuTrash2 />}>
Deactivate
</Button>
</>
);
export function AllSelected() {
return (
<BulkActionBar
selection={args.selection}
isAllSelected
onClearSelection={args.onClearSelection}
>
{actions}
</BulkActionBar>
);
}
With table
With a Table: the bar is rendered only while rows are selected, at the bottom of the viewport (sticky). Select rows with the checkboxes (or Space) and listen to the polite announcement; "Clear selection" removes the bar.
import { useState } from "react";
import { type Selection } from "react-aria-components";
import {
BulkActionBar,
Button,
SearchField,
Table,
TableBody,
TableCell,
TableHeader,
TableHeaderCell,
TableRow,
TableToolbar,
} from "@nexera-ui/react";
import { LuDownload, LuMessageSquare, LuTrash2, LuUsers } from "react-icons/lu";
const actions = (
<>
<Button variant="secondary" leadingIcon={<LuMessageSquare />}>
Message
</Button>
<Button variant="secondary" leadingIcon={<LuDownload />}>
Export
</Button>
<Button variant="secondary" leadingIcon={<LuUsers />}>
Change team
</Button>
<Button variant="destructive" leadingIcon={<LuTrash2 />}>
Deactivate
</Button>
</>
);
interface Row {
readonly id: string;
readonly name: string;
readonly role: string;
}
const rows: readonly Row[] = [
{ id: "ayesha", name: "Ayesha Khan", role: "Product designer" },
{ id: "bilal", name: "Bilal Ahmed", role: "Engineering manager" },
{ id: "sana", name: "Sana Iqbal", role: "People operations" },
{ id: "omar", name: "Omar Farooq", role: "Data analyst" },
];
function SelectableTable() {
const [selected, setSelected] = useState<Selection>(new Set(["ayesha", "sana"]));
const count = selected === "all" ? rows.length : selected.size;
const clear = () => {
setSelected(new Set());
};
return (
<div className="flex flex-col gap-3">
<TableToolbar
aria-label="Employees table tools"
search={<SearchField aria-label="Search employees" placeholder="Search" />}
/>
<Table
aria-label="Employees"
selectionMode="multiple"
selectedKeys={selected}
onSelectionChange={setSelected}
>
<TableHeader>
<TableHeaderCell id="name" isRowHeader label="Name" />
<TableHeaderCell id="role" label="Role" />
</TableHeader>
<TableBody items={rows}>
{(row) => (
<TableRow id={row.id}>
<TableCell>{row.name}</TableCell>
<TableCell>{row.role}</TableCell>
</TableRow>
)}
</TableBody>
</Table>
{count > 0 ? (
<div className="sticky bottom-4 flex justify-center">
<BulkActionBar
selection={`${String(count)} of ${String(rows.length)} selected`}
isAllSelected={count === rows.length}
onClearSelection={clear}
onSelectAll={() => {
setSelected("all");
}}
selectAllLabel={`Select all ${String(rows.length)}`}
>
{actions}
</BulkActionBar>
</div>
) : null}
</div>
);
}
export function WithTable() {
return <SelectableTable />;
}
Narrow container
At 20 rem the parts wrap onto several lines and the bar keeps inside its container.
import { BulkActionBar, Button } from "@nexera-ui/react";
import { LuDownload, LuMessageSquare, LuTrash2, LuUsers } from "react-icons/lu";
const actions = (
<>
<Button variant="secondary" leadingIcon={<LuMessageSquare />}>
Message
</Button>
<Button variant="secondary" leadingIcon={<LuDownload />}>
Export
</Button>
<Button variant="secondary" leadingIcon={<LuUsers />}>
Change team
</Button>
<Button variant="destructive" leadingIcon={<LuTrash2 />}>
Deactivate
</Button>
</>
);
<div className="max-w-xs">
<BulkActionBar
selection="3 of 128 selected"
onClearSelection={() => undefined}
onSelectAll={() => undefined}
selectAllLabel="Select all 128"
>
{actions}
</BulkActionBar>
</div>;
Long content
Long and translated text wraps instead of truncating.
import { BulkActionBar, Button } from "@nexera-ui/react";
import { LuDownload, LuMessageSquare, LuTrash2, LuUsers } from "react-icons/lu";
const actions = (
<>
<Button variant="secondary" leadingIcon={<LuMessageSquare />}>
Message
</Button>
<Button variant="secondary" leadingIcon={<LuDownload />}>
Export
</Button>
<Button variant="secondary" leadingIcon={<LuUsers />}>
Change team
</Button>
<Button variant="destructive" leadingIcon={<LuTrash2 />}>
Deactivate
</Button>
</>
);
<div className="max-w-xl">
<BulkActionBar
selection="3 von 128 Mitarbeitenden ausgewaehlt, einschliesslich zwei Personen im Urlaub"
onClearSelection={() => undefined}
onSelectAll={() => undefined}
selectAllLabel="Alle 128 Mitarbeitenden auswaehlen"
>
{actions}
</BulkActionBar>
</div>;
Right to left
Right-to-left: the checkbox sits at the start (right), the clear button at the end (left), and arrow keys mirror.
import { BulkActionBar, Button } from "@nexera-ui/react";
import { LuDownload, LuTrash2 } from "react-icons/lu";
<BulkActionBar
selection="تم تحديد 3 من 128"
onClearSelection={() => undefined}
onSelectAll={() => undefined}
selectAllLabel="تحديد الكل 128"
clearSelectionLabel="إلغاء التحديد"
aria-label="إجراءات جماعية"
>
{
<>
<Button variant="secondary" leadingIcon={<LuDownload />}>
تصدير
</Button>
<Button variant="destructive" leadingIcon={<LuTrash2 />}>
تعطيل
</Button>
</>
}
</BulkActionBar>;
Props 10
Press "Try it" on a card to load that prop into the playground.
10 props shown
selection*NexerastringThe selection count as a sentence. It is shown, it names the checkbox, and it is announced politely whenever it changes and when the bar appears (WCAG 4.1.3). Format it for your locale ("3 von 128 ausgewaehlt"); keep it free of markup.
isAllSelectedNexerabooleanEvery row is selected: the checkbox shows checked instead of the Figma indeterminate dash.
onClearSelection*Nexera() => voidCalled when the checkbox or the "Clear selection" button is pressed. The bar should disappear once nothing is selected.
onSelectAllNexera() => voidCalled by the "Select all" button, which shows when this is provided. Offer it while only some rows are selected, for example when the table is paged.
selectAllLabelNexerastringText of the "Select all" button; put the total in it ("Select all 128") so the effect is clear. Translate it.
clearSelectionLabelNexerastringAccessible name of the "Clear selection" icon button. Translate it.
aria-labelNexerastringAccessible name of the toolbar. Translate it.
childrenNexeraReactNodeThe actions for the selected rows. Arrow keys move between them. Keep destructive actions confirmable.
classNameNexerastringExtra classes, merged last so they win over the defaults.
aria-describedbyReact AriastringIdentifies the element (or elements) that describes the object.
* Required. React Aria props shown are the ones most apps use; the component accepts the rest of its React Aria props too.
Accessible by default.
Built on React Aria, and covered by the WCAG 2.2 evidence generated on every build.
WCAG 2.2 evidence
7 direct · 4 supporting- 1.3.1Info and RelationshipsLevel A · tested directly
- 1.3.2Meaningful SequenceLevel A · tested directly
- 1.4.1Use of ColorLevel A · tested directly
- 1.4.10ReflowLevel AA · supporting test
- 1.4.12Text SpacingLevel AA · supporting test
- 2.1.1KeyboardLevel A · tested directly
- 2.4.3Focus OrderLevel A · tested directly
- 2.4.7Focus VisibleLevel AA · supporting test
- 2.5.8Target Size (Minimum)Level AA · supporting test
- 4.1.2Name, Role, ValueLevel A · tested directly
- 4.1.3Status MessagesLevel AA · tested directly
Fits any width.
Nexera components respond to the space they are given. Drag the corner of the frame, or pick a width.
Styling hooks
Pass className to add Tailwind classes (merged last). State is exposed as data attributes, so you can style it with variants like data-pressed:.
data-disableddata-hovereddata-pressed
<BulkActionBar className="data-disabled:opacity-90 shadow-sm" />Used in blocks
Related components
- AccordionItemOne collapsible section of an FAQ or settings page.
- AreaChartA stacked area chart: layers piled up over time with the title, range switch, actions and legend of the card.
- AvailabilityRowOne person's availability across a window of the day: an avatar with initials and the name, and a timeline with the busy periods drawn as bars.
- AvatarA person or organisation shown as a photo, initials or an icon.
- AvatarGroupA row of overlapping avatars with a "+N" chip for the rest.
- AvatarLabelAn avatar with a name and an optional subtitle beside it, for a person in a list, a header or a mention.
- BadgeShort status or category label: `Tone` x `Variant` x `Size`, with an optional dot, leading icon and remove button.
- BadgeGroupA pill that pairs a small badge with a short message, for an announcement or a status line.