Bulk Action Toolbar
table-toolbar-ui toolbar-ui table-ui
When a table has row selection, swap the standard <table-toolbar-ui> for a contextual bulk-action bar at the same surface position. Selection count anchors the left, actions flow from the right, destructive action last.
When to use
Any table where users act on multiple rows at once: archive, delete, export, move.
Swapping the toolbar (rather than stacking a second bar) keeps the surface compact and
signals "selection mode" with the strongest possible affordance — the chrome itself changes.
Toolbar swap on selection
Tick the checkboxes below — the default <table-toolbar-ui> swaps for the bulk-action bar at the same height. Untick to return.
Anatomy
Both toolbars sit inside the same <section bleed> so they share one surface. Toggle visibility on the select event from the table.
<card-ui>
<section bleed>
<!-- Default state — modern foundation -->
<table-toolbar-ui for="resources" text="Resources" count="6"></table-toolbar-ui>
<!-- Bulk action bar — swaps in when rows are selected -->
<toolbar-ui data-bulk-bar hidden role="toolbar" aria-label="Bulk actions">
<text-ui strong><span data-bulk-count>0</span> selected</text-ui>
<span data-spacer></span>
<button-ui text="Archive" icon="archive" variant="ghost" size="sm"></button-ui>
<button-ui text="Export" icon="download-simple" variant="ghost" size="sm"></button-ui>
<button-ui text="Move" icon="folder-simple" variant="ghost" size="sm"></button-ui>
<divider-ui vertical></divider-ui>
<button-ui text="Delete" icon="trash" variant="ghost" size="sm"></button-ui>
</toolbar-ui>
<table-ui id="resources" selectable sortable></table-ui>
</section>
</card-ui>
// Wire the swap. table-ui dispatches 'select' with detail.selected (number[]).
const table = document.getElementById('resources');
const tbar = document.querySelector('table-toolbar-ui');
const bulk = document.querySelector('[data-bulk-bar]');
const countEl = bulk.querySelector('[data-bulk-count]');
table.addEventListener('select', (e) => {
const n = e.detail.selected?.length ?? 0;
countEl.textContent = String(n);
tbar.hidden = n > 0;
bulk.hidden = n === 0;
});
Rules
- Default state uses
<table-toolbar-ui> — the modern foundation. It already wires Filter / Sort / Columns / Search to the table via [for]; don't hand-roll those.
- Bulk bar uses
<toolbar-ui> with role="toolbar" and aria-label="Bulk actions". Match its --toolbar-py / --toolbar-px to --table-toolbar-py / --table-toolbar-px so the swap is seamless.
- Both toolbars sit inside the same
<section bleed> as the table — one surface, flush alignment with the first table cell.
- Selection count leads the bulk bar;
<span data-spacer> pushes actions right.
- Destructive actions go last and are visually separated with a
<divider-ui vertical>.
- Keep bulk actions to 3–5; overflow belongs in a
<popover-ui> "More" menu trailing the divider.
- Drive the swap from the table's
select event (detail.selected: number[]) — toggle hidden on each toolbar; don't unmount.