Bulk Actions Bar — Table Row Selection HTML CSS JS

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

Share & Support

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.

Build with AI

Build, Understand, Optimize, and Extend It With AI

You don't have to trace every sync() call by hand to see why the selection state never drifts from the UI. Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly why the selected ids are stored in a Set rather than an array, and how sync computes the header checkbox's indeterminate property (which is JavaScript-only, not an HTML attribute) from the relationship between selected.size and ROWS.length. The same assistant can help optimize it — asking whether re-querying every table row on every single sync call is wasteful for a table with thousands of rows compared to only touching changed rows, or whether the delete action's full-table re-render could instead remove just the affected DOM nodes. It's also useful for extending the pattern: ask it to add Shift-click range selection, keyboard navigation between checkboxes, or an undo toast after a bulk delete. Treat the code less like a finished artifact and more like a starting point for a conversation.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a data table with row selection and a floating "bulk actions bar" in plain HTML, CSS, and JavaScript, backed by a Set for selection state — no framework, no library.

Requirements:
- A table with a checkbox in the header (select-all) and a checkbox in every row, where selected row ids are stored in a single JavaScript Set that is the sole source of truth for selection.
- A single sync function that, given only the current Set, updates everything derived from it in one pass: the visible "N selected" count, whether each row carries a selected/highlighted class, whether each row's own checkbox is checked, and the header checkbox's checked and indeterminate properties (checked only when every row is selected, indeterminate specifically when some but not all rows are selected, unchecked when none are) — no other code path may directly mutate any of these outside that one function.
- A bar that is completely hidden (not clickable, not visible) when the Set is empty, and slides up into view the moment the Set has at least one entry, sliding away again the instant it's cleared, animated using only transform and opacity so it composites smoothly.
- Toggling the header checkbox must add every row's id to the Set (selecting all) or clear the Set entirely (selecting none), and clicking a single row checkbox must add or remove just that row's id, both cases calling the same sync function afterward.
- The bar must contain at least three action buttons (e.g. email, export, delete) that read Array.from(selected) to know which ids to act on; the delete action specifically must remove the corresponding rows from both the underlying data array and the DOM, then clear the Set and re-sync, while the other actions show a brief confirmation state before clearing the selection.
- Provide a separate "Clear" control that empties the Set and re-syncs without performing any action, and ensure every row checkbox and the select-all checkbox carry proper aria-labels naming what they select.

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.

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.