webdevpuneet.com

Bulk Actions Bar — Table Row Selection HTML CSS JS

Bulk Actions Bar · Tables · Plain HTML, CSS & JS · Live preview

CategoryTables

Copy & paste

Source Code

<div class="bab-card">
  <h3>Subscribers</h3>
  <div class="bab-table-wrap">
    <table class="bab-table">
      <thead>
        <tr>
          <th class="bab-cb"><input type="checkbox" id="babAll" aria-label="Select all"></th>
          <th>Name</th><th>Email</th><th>Status</th>
        </tr>
      </thead>
      <tbody id="babBody"></tbody>
    </table>
  </div>

  <!-- Floating bar appears when one or more rows are selected -->
  <div class="bab-bar" id="babBar">
    <span class="bab-count"><b id="babCount">0</b> selected</span>
    <div class="bab-actions">
      <button type="button" data-act="email"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="M22 7l-10 5L2 7"/></svg> Email</button>
      <button type="button" data-act="export"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg> Export</button>
      <button type="button" data-act="delete" class="bab-danger"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/></svg> Delete</button>
    </div>
    <button type="button" class="bab-clear" id="babClear">Clear</button>
  </div>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

Contextual floating bar
Hidden until rows are selected, then slides up with the count and actions — and slides away when cleared.
Set-backed selection
Selected ids live in a Set that's the single source of truth; one sync() projects it onto the whole UI.
Tri-state select-all
The header checkbox shows checked (all), indeterminate (some), or unchecked (none) — the accessible standard.
Row highlight
Selected rows tint so the selection is visible in the table itself, not only in the bar's count.
Email / Export / Delete actions
Each operates on the current selection, with Delete removing rows and visually separated as destructive.
One-click clear
A Clear button deselects everything without unchecking each row individually.
Spring-eased reveal
The bar animates with transform and opacity only, staying smooth across every framework export.
Accessible checkboxes
Per-row aria-labels and a labeled select-all make the selection operable and announced for assistive tech.

About this UI Snippet

Bulk Actions Bar — Floating Action Bar for Selected Table Rows

Screenshot of the Bulk Actions Bar snippet rendered live

Any table where users manage many records — subscribers, orders, files, tasks — needs bulk actions: select several rows, then email, export, or delete them all at once. The pattern that does this best is a floating action bar that stays hidden until something is selected, then slides up with the count and the available actions. This snippet builds that complete interaction in plain HTML, CSS, and vanilla JavaScript: row checkboxes, a header select-all with an indeterminate state, and a context bar that appears on demand.

A bar that appears only when relevant

The bulk-actions bar is hidden by default and slides up from the bottom of the table the instant one or more rows are selected, showing "N selected" and the action buttons. When the selection is cleared, it slides away. This "contextual toolbar" approach keeps the interface clean — the actions don't clutter the screen when there's nothing to act on, and they appear exactly when they become useful. The bar animates with transform and opacity only (a spring-eased rise), so it feels responsive and stays smooth across every framework export.

Selection tracked in a Set

Selected rows live in a JavaScript Set of row ids, which is the natural data structure for "which things are chosen": adding, removing, and membership checks are all trivial, and selected.size is the live count. Every interaction — a row checkbox, select-all, or an action — mutates the Set and calls one sync() function that re-derives all the UI: the count, the bar's visibility, each row's highlight and checkbox, and the header checkbox state. There's no scattered DOM bookkeeping; the Set is the source of truth and the UI is its projection.

Select-all with a true indeterminate state

The header checkbox does more than check/uncheck everything — it correctly reflects partial selection with the indeterminate state (the dash, not a check), which is the standard, accessible way to show "some but not all rows are selected." sync() sets checked when every row is selected, indeterminate when some are, and neither when none are. This three-state header is a detail most hand-built tables skip, but it's what makes select-all feel correct: a half-selected table shouldn't show a fully-checked header.

Actions that operate on the selection

The bar's buttons — Email, Export, and a red-tinted Delete — each operate on the current set of selected ids. Delete removes the rows and clears the selection (which hides the bar); Email and Export show a brief confirmation in the demo, but in a real app these are where you'd call your API with the selected ids. Separating the destructive Delete visually (red on hover) follows the convention that keeps a bulk delete from being fired by accident, and a "Clear" button lets users deselect everything in one click without unchecking each row.

Row affordances and accessibility

Selected rows get a tinted background so the selection is visible in the table itself, not just in the bar's count, and each checkbox carries an aria-label naming its row. The header checkbox is labeled "Select all." For a production build you'd add Shift-click range selection (covered in the FAQs) and announce selection changes via a live region, but the core — Set-backed selection, a contextual bar, and a correct tri-state header — is the foundation every bulk-action table needs.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSA subscribers table renders with a checkbox per row and a select-all in the header.
  2. 2
    Select rowsCheck one or more rows — a floating bar slides up showing the count and Email / Export / Delete actions.
  3. 3
    Use select-allClick the header checkbox to select every row; with a partial selection it shows the indeterminate dash.
  4. 4
    Run a bulk actionClick Email, Export, or Delete — the action applies to all selected rows (Delete removes them and hides the bar).
  5. 5
    Clear the selectionClick "Clear" to deselect everything at once; the bar slides away.
  6. 6
    Wire up real actionsIn the actions handler, call your API with Array.from(selected) for email, export, or delete.

Real-world uses

Common Use Cases

Admin and CRM tables
Manage subscribers, contacts, or users with bulk email, tag, or delete — pair with a data table column toggle.
Order and inventory management
Select multiple orders to fulfill, export, or cancel in one action.
File and media managers
Bulk move, download, or delete selected files.
Email and campaign tools
Select recipients or segments for a bulk send, complementing a multi email input.
Task and project lists
Mark several tasks done, assign, or archive at once, alongside a sortable table.
Learning selection UX
A reference for Set-backed selection, contextual toolbars, and tri-state select-all — compare with a pagination table for paged data.
Related: Compare Products Table
See the Compare Products Table for a related tables pattern worth pairing with this one.
Related: Live Market Watchlist Table
See the Live Market Watchlist Table for a related tables pattern worth pairing with this one.
Related: Cell Range Select & Copy Table
See the Cell Range Select & Copy Table for a related tables pattern worth pairing with this one.
Related: Table with Keyboard Cell Navigation
See the Table with Keyboard Cell Navigation for a related tables pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

A Set of row ids is the natural model for "which rows are chosen": add/delete/has are O(1), there are no duplicates, and size gives the live count. Every interaction mutates the Set and calls one sync() that re-derives the entire UI from it — the count, bar visibility, row highlights, and header state — so the displayed selection can never drift from the actual selection. Storing it as an array or scattered DOM flags invites those inconsistencies.

A checkbox's indeterminate is a JavaScript-only property (not an attribute) that shows a dash instead of a check. In sync(), set checked = (selected.size === total), and indeterminate = (selected.size > 0 && selected.size < total). This gives the correct three states — all, some, none — so a partial selection shows the dash rather than a misleading full check, which is the accessible, expected behavior.

Track the index of the last-clicked row. On a row checkbox change, if Shift was held (check the event), select every row between the last index and the current one (in either direction) by adding their ids to the Set, then sync(). This lets users select a contiguous range quickly, matching the file-manager convention, and only requires remembering the previous click index.

In the actions click handler, take Array.from(selected) (the chosen ids) and call your API — a bulk email endpoint, an export that streams a file, or a batch delete. Show a loading state during the request and reconcile the table with the result (remove deleted rows, show a success toast). For destructive actions, add a confirmation step before firing.

In React, hold the selected Set (or array) in useState and derive the count, bar visibility, and header state in render; in Vue, use a reactive Set with computed values; in Angular, use a component Set with getters. The selection logic and tri-state header math port unchanged — only the per-change re-render moves into the framework's reactivity, and you set the checkbox indeterminate via a ref/directive since it's a property.