BulkActionBar
Floating toolbar for ticked rows: "3 selected · RM 34,890.00", Select all N / Clear, the main actions as buttons and the rest in a ⋯ menu.
import { BulkActionBar } from "@cynco-labs/ui"; Example
import { useState } from "react";
import { ArrowSquareOutIcon, CopyIcon, CreditCardIcon, TrashIcon } from "@phosphor-icons/react";
import {
BulkActionBar,
Checkbox,
DataList,
DocStatus,
INVOICE_STATUS,
Money,
formatCurrency,
formatDate,
sumAmounts,
} from "@cynco-labs/ui";
const INV = [
{
id: "1",
no: "INV-2026-0142",
customer: "Syarikat Maju Jaya Sdn Bhd",
due: "2026-11-12",
amount: "12850.00",
status: "finalized",
},
{
id: "2",
no: "INV-2026-0141",
customer: "Tropika Kopi Enterprise",
due: "2026-10-30",
amount: "3240.00",
status: "overdue",
},
{
id: "3",
no: "INV-2026-0140",
customer: "Nusantara Logistik Bhd",
due: "2026-11-05",
amount: "18800.00",
status: "finalized",
},
{
id: "4",
no: "INV-2026-0139",
customer: "Pelita Hartanah Sdn Bhd",
due: "2026-10-21",
amount: "7450.50",
status: "overdue",
},
{
id: "5",
no: "INV-2026-0138",
customer: "Batik Warisan Trading",
due: "2026-11-18",
amount: "2190.00",
status: "finalized",
},
{
id: "6",
no: "INV-2026-0137",
customer: "Klinik Sentosa Sdn Bhd",
due: "2026-11-20",
amount: "5600.00",
status: "draft",
},
{
id: "7",
no: "INV-2026-0136",
customer: "Cahaya Solar Teknik",
due: "2026-10-15",
amount: "21375.25",
status: "overdue",
},
{
id: "8",
no: "INV-2026-0135",
customer: "Restoran Selera Kampung",
due: "2026-11-25",
amount: "980.00",
status: "finalized",
},
];
const cols = [
{
key: "no",
header: "Invoice #",
mobile: "title",
nowrap: true,
cell: (r) => <span className="font-mono text-xs text-primary">{r.no}</span>,
},
{ key: "customer", header: "Customer", cell: (r) => r.customer },
{ key: "due", header: "Due", nowrap: true, cell: (r) => formatDate(r.due) },
{
key: "status",
header: "Status",
mobile: "badge",
cell: (r) => <DocStatus status={r.status} config={INVOICE_STATUS} />,
},
{
key: "amount",
header: "Amount",
align: "right",
mobile: "value",
cell: (r) => <Money amount={r.amount} />,
},
];
export default function BulkActionBarDemo() {
const [sel, setSel] = useState(["1", "3", "5"]);
const [log, setLog] = useState(
"Try: tick rows · Esc clears · P / R / D run actions · ← → move along the bar · ⋯ for more",
);
const picked = INV.filter((r) => sel.indexOf(r.id) >= 0);
const total = sumAmounts(picked.map((r) => r.amount));
function run(verb) {
return () => {
setLog(
verb +
" " +
picked.length +
" invoice" +
(picked.length === 1 ? "" : "s") +
" (" +
formatCurrency(total) +
")",
);
};
}
const actions = [
{
id: "pay",
label: "Record payment",
icon: <CreditCardIcon size={14} />,
shortcut: "p",
onSelect: run("Recorded payment for"),
},
{
id: "remind",
label: "Send reminder",
icon: <ArrowSquareOutIcon size={14} />,
shortcut: "r",
onSelect: run("Sent reminders for"),
},
{
id: "pdf",
label: "Download PDFs",
icon: <CopyIcon size={14} />,
shortcut: "d",
onSelect: run("Downloading PDFs for"),
},
{ id: "dup", label: "Duplicate", icon: <CopyIcon size={14} />, onSelect: run("Duplicated") },
{
id: "csv",
label: "Export to CSV",
icon: <ArrowSquareOutIcon size={14} />,
shortcut: "mod+e",
onSelect: run("Exported"),
},
{
id: "void",
label: "Void",
icon: <TrashIcon size={14} />,
destructive: true,
shortcut: "shift+v",
onSelect: () => {
run("Voided")();
setSel([]);
},
},
];
const all = sel.length === INV.length;
return (
<div className="flex flex-col gap-3">
<p className="text-xs text-tertiary-foreground" role="status">
{log}
</p>
<div className="max-h-[330px] overflow-y-auto rounded-xl">
<DataList
columns={cols}
rows={INV}
getKey={(r) => r.id}
caption="Invoices"
leadingHeader={
<Checkbox
checked={all}
indeterminate={sel.length > 0 && !all}
onCheckedChange={(v) => {
setSel(v ? INV.map((r) => r.id) : []);
}}
aria-label="Select all invoices"
/>
}
leading={(r) => (
<Checkbox
checked={sel.indexOf(r.id) >= 0}
onCheckedChange={(v) => {
setSel(v ? sel.concat(r.id) : sel.filter((x) => x !== r.id));
}}
aria-label={"Select " + r.no}
/>
)}
isRowSelected={(r) => sel.indexOf(r.id) >= 0}
/>
<BulkActionBar
className="mt-2"
count={sel.length}
total={total}
totalCount={INV.length}
onSelectAll={() => {
setSel(INV.map((r) => r.id));
}}
onClear={() => {
setSel([]);
}}
actions={actions}
/>
</div>
<div className="w-full max-w-[340px] rounded-xl border border-dashed border-border p-2">
<BulkActionBar
sticky={false}
count={sel.length || 2}
total={sel.length ? total : "16090.00"}
totalCount={INV.length}
onSelectAll={() => {
setSel(INV.map((r) => r.id));
}}
onClear={() => {
setSel([]);
}}
actions={actions.slice(0, 2).concat(actions.slice(4))}
/>
</div>
</div>
);
} Use
Under any DataList with checkboxes. Render it after the list inside the scrolling container; it sticks to the bottom while rows scroll and disappears at 0 selected. Compute total with sumAmounts over the selected rows' money field.
Props
count, onClear, actions ([{id, label, icon?, onSelect, destructive?, shortcut? ("p", "shift+v", "mod+e"), disabled?, overflow? (always in the ⋯ menu)}]); optional total (decimal string), currency (default MYR), totalCount + onSelectAll (shows "Select all N"), maxVisible (buttons before the ⋯ menu, default 3), sticky (default true), label (toolbar name), className and any div props.
Notes
The count/total is aria-live. Responsive: one row from 48rem of container width (keycaps shown), below that the summary + Select all/Clear sit above the actions, and under 24rem the summary takes its own line; hidden in print.