Open demo

CSS code

HTML
<section class="sel-card" aria-labelledby="sel-title">
  <div class="sel-bar">
    <h2 id="sel-title">Team members</h2>
    <div class="sel-bulk" data-sel-bulk><span data-sel-count></span><button class="sel-btn" type="button">Export</button><button class="sel-btn danger" type="button">Remove</button></div>
  </div>
  <p class="sr-only" role="status" data-sel-status></p>
  <div class="sel-scroll" role="region" tabindex="0" aria-label="Team members, scrollable">
    <table class="sel" aria-labelledby="sel-title">
      <thead><tr><td class="sel-check"><input type="checkbox" data-sel-all aria-label="Select all team members"></td><th scope="col">Name</th><th scope="col">Email</th><th scope="col">Role</th></tr></thead>
      <tbody>
        <tr><td class="sel-check"><input type="checkbox" data-sel-row aria-label="Select Maja Lindgren"></td><th scope="row">Maja Lindgren</th><td class="sel-email">[email protected]</td><td><span class="sel-role admin">Admin</span></td></tr>
        <tr><td class="sel-check"><input type="checkbox" data-sel-row aria-label="Select Diego Fuentes"></td><th scope="row">Diego Fuentes</th><td class="sel-email">[email protected]</td><td><span class="sel-role">Editor</span></td></tr>
        <tr><td class="sel-check"><input type="checkbox" data-sel-row aria-label="Select Amara Obi"></td><th scope="row">Amara Obi</th><td class="sel-email">[email protected]</td><td><span class="sel-role">Editor</span></td></tr>
        <tr><td class="sel-check"><input type="checkbox" data-sel-row aria-label="Select Jonas Weber"></td><th scope="row">Jonas Weber</th><td class="sel-email">[email protected]</td><td><span class="sel-role">Viewer</span></td></tr>
        <tr><td class="sel-check"><input type="checkbox" data-sel-row aria-label="Select Yuki Tanaka"></td><th scope="row">Yuki Tanaka</th><td class="sel-email">[email protected]</td><td><span class="sel-role">Viewer</span></td></tr>
      </tbody>
    </table>
  </div>
</section>
CSS
/* Selectable rows: a named checkbox per row, a select-all checkbox that goes indeterminate on a partial selection,
   and a bulk-action bar that appears once something is selected. A visually hidden status line announces the count. */
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
.sel-card{position:relative;width:100%;max-width:720px;background:#fff;color:#182030;border:1px solid #e5eaf2;border-radius:16px;overflow:hidden;box-shadow:0 14px 32px -18px rgba(18,28,48,.16)}
.sel-bar{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:10px;padding:15px 20px;border-bottom:1px solid #ecf0f6;min-height:62px}
.sel-bar h2{font-size:1rem;font-weight:700;letter-spacing:-.01em;margin:0}
.sel-bulk{display:none;align-items:center;gap:10px}
.sel-bulk.show{display:flex}
.sel-bulk span{font-size:.85rem;font-weight:600;color:#3d4a63}
.sel-btn{border:1px solid #d7deea;background:#fff;font:inherit;font-size:.8rem;font-weight:600;padding:7px 13px;border-radius:8px;cursor:pointer;color:#3d4a63;transition:background-color .15s}
.sel-btn:hover{background:#f2f5fa}
.sel-btn.danger{color:#b42323;border-color:#f1c8c8}
.sel-btn.danger:hover{background:#fdf1f1}
.sel-btn:focus-visible,.sel input:focus-visible{outline:2px solid #2563eb;outline-offset:2px}
.sel-scroll{position:relative;overflow-x:auto}
.sel-scroll:focus-visible{outline:2px solid #2563eb;outline-offset:-2px}
.sel{width:100%;border-collapse:collapse;font-size:.88rem}
.sel th,.sel td{text-align:left}
.sel thead th{font-size:.72rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:#5f6b82;padding:11px 20px;background:#f8fafc;border-bottom:1px solid #ecf0f6}
.sel tbody th,.sel td{padding:12px 20px;border-bottom:1px solid #f2f5f9;color:#34405a}
.sel tbody tr:last-child>*{border-bottom:0}
.sel .sel-check{width:1px;padding-right:0}
.sel input[type=checkbox]{display:block;width:16px;height:16px;margin:0;accent-color:#2563eb;cursor:pointer}
.sel tbody tr:hover{background:#f7fafd}
.sel tbody tr.is-selected{background:#eff4ff}
.sel tbody tr.is-selected:hover{background:#e8effd}
.sel tbody th{font-weight:600;color:#182030}
.sel-email{color:#5f6b82}
.sel-role{font-size:.75rem;font-weight:700;padding:3px 10px;border-radius:999px;background:#eef1f6;color:#4a5670;white-space:nowrap}
.sel-role.admin{background:#e7edff;color:#2b4acb}
@media (prefers-reduced-motion:reduce){.sel-btn{transition:none}}
JavaScript
// Row checkboxes drive the row highlight, the select-all state (checked / indeterminate) and the bulk bar.
(function () {
  var card = document.querySelector('.sel-card');
  if (!card) return;
  var all = card.querySelector('[data-sel-all]');
  var boxes = Array.prototype.slice.call(card.querySelectorAll('[data-sel-row]'));
  var bulk = card.querySelector('[data-sel-bulk]');
  var count = card.querySelector('[data-sel-count]');
  var status = card.querySelector('[data-sel-status]');
  function sync() {
    var n = boxes.filter(function (b) { return b.checked; }).length;
    boxes.forEach(function (b) { b.closest('tr').classList.toggle('is-selected', b.checked); });
    bulk.classList.toggle('show', n > 0);
    count.textContent = n + ' selected';
    status.textContent = n ? n + ' of ' + boxes.length + ' selected' : 'No rows selected';
    all.checked = n === boxes.length;
    all.indeterminate = n > 0 && n < boxes.length;
  }
  boxes.forEach(function (b) { b.addEventListener('change', sync); });
  all.addEventListener('change', function () { boxes.forEach(function (b) { b.checked = all.checked; }); sync(); });
})();

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<section class="sel-card position-relative w-100" aria-labelledby="sel-title">
  <div class="sel-bar d-flex align-items-center justify-content-between flex-wrap gap-2">
    <h2 id="sel-title">Team members</h2>
    <div class="sel-bulk align-items-center gap-2" data-sel-bulk hidden><span data-sel-count></span><button class="btn" type="button">Export</button><button class="btn btn-outline-danger" type="button">Remove</button></div>
  </div>
  <p class="visually-hidden" role="status" data-sel-status></p>
  <div class="sel-scroll table-responsive position-relative" role="region" tabindex="0" aria-label="Team members, scrollable">
    <table class="table table-hover sel align-middle mb-0" aria-labelledby="sel-title">
      <thead><tr><td class="sel-check"><input class="form-check-input" type="checkbox" data-sel-all aria-label="Select all team members"></td><th scope="col">Name</th><th scope="col">Email</th><th scope="col">Role</th></tr></thead>
      <tbody>
        <tr><td class="sel-check"><input class="form-check-input" type="checkbox" data-sel-row aria-label="Select Maja Lindgren"></td><th scope="row">Maja Lindgren</th><td class="sel-email">[email protected]</td><td><span class="badge rounded-pill admin">Admin</span></td></tr>
        <tr><td class="sel-check"><input class="form-check-input" type="checkbox" data-sel-row aria-label="Select Diego Fuentes"></td><th scope="row">Diego Fuentes</th><td class="sel-email">[email protected]</td><td><span class="badge rounded-pill">Editor</span></td></tr>
        <tr><td class="sel-check"><input class="form-check-input" type="checkbox" data-sel-row aria-label="Select Amara Obi"></td><th scope="row">Amara Obi</th><td class="sel-email">[email protected]</td><td><span class="badge rounded-pill">Editor</span></td></tr>
        <tr><td class="sel-check"><input class="form-check-input" type="checkbox" data-sel-row aria-label="Select Jonas Weber"></td><th scope="row">Jonas Weber</th><td class="sel-email">[email protected]</td><td><span class="badge rounded-pill">Viewer</span></td></tr>
        <tr><td class="sel-check"><input class="form-check-input" type="checkbox" data-sel-row aria-label="Select Yuki Tanaka"></td><th scope="row">Yuki Tanaka</th><td class="sel-email">[email protected]</td><td><span class="badge rounded-pill">Viewer</span></td></tr>
      </tbody>
    </table>
  </div>
</section>
CSS
/* Bootstrap .table-hover with .form-check-input checkboxes; a selected row gets .table-active,
   recoloured through --bs-table-active-bg. The bulk bar uses .btn, the count is announced by a .visually-hidden status. */
.sel-card{max-width:720px;background:#fff;color:#182030;border:1px solid #e5eaf2;border-radius:16px;overflow:hidden;box-shadow:0 14px 32px -18px rgba(18,28,48,.16)}
.sel-bar{padding:15px 20px;border-bottom:1px solid #ecf0f6;min-height:62px}
.sel-bar h2{font-size:1rem;font-weight:700;letter-spacing:-.01em;line-height:normal;margin:0}
.sel-bulk span{font-size:.85rem;font-weight:600;color:#3d4a63}
.sel-bar .btn{--bs-btn-font-size:.8rem;--bs-btn-font-weight:600;--bs-btn-line-height:normal;--bs-btn-padding-x:13px;--bs-btn-padding-y:7px;--bs-btn-border-radius:8px;
  --bs-btn-color:#3d4a63;--bs-btn-bg:#fff;--bs-btn-border-color:#d7deea;--bs-btn-hover-color:#3d4a63;--bs-btn-hover-bg:#f2f5fa;--bs-btn-hover-border-color:#d7deea;
  --bs-btn-active-color:#3d4a63;--bs-btn-active-bg:#e9eef6;--bs-btn-active-border-color:#d7deea;--bs-btn-focus-box-shadow:0 0 0 2px #fff,0 0 0 4px #2563eb}
.sel-bar .btn-outline-danger{--bs-btn-color:#b42323;--bs-btn-border-color:#f1c8c8;--bs-btn-hover-color:#b42323;--bs-btn-hover-bg:#fdf1f1;--bs-btn-hover-border-color:#f1c8c8;
  --bs-btn-active-color:#b42323;--bs-btn-active-bg:#fbe4e4;--bs-btn-active-border-color:#f1c8c8}
.sel-scroll:focus-visible{outline:2px solid #2563eb;outline-offset:-2px}
.sel{--bs-table-bg:#fff;--bs-table-color:#34405a;--bs-table-border-color:#f2f5f9;--bs-table-hover-bg:#f7fafd;--bs-table-hover-color:#34405a;
  --bs-table-active-bg:#eff4ff;--bs-table-active-color:#34405a;font-size:.88rem}
.sel>tbody>tr>*{padding:12px 20px}
.sel>thead>tr>*{--bs-table-bg:#f8fafc;--bs-table-color:#5f6b82;font-size:.72rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;padding:11px 20px;border-bottom-color:#ecf0f6}
.sel>tbody>tr:last-child>*{border-bottom-width:0}
.sel>tbody>tr.table-active:hover>*{--bs-table-bg-state:#e8effd}
.sel .sel-check{width:1px;padding-right:0}
.sel .form-check-input{display:block;width:16px;height:16px;margin:0;border-color:#7a8699;cursor:pointer;--bs-form-check-bg:#fff}
.sel .form-check-input:checked,.sel .form-check-input:indeterminate{background-color:#2563eb;border-color:#2563eb}
.sel .form-check-input:focus{box-shadow:none;border-color:#7a8699}
.sel .form-check-input:checked:focus,.sel .form-check-input:indeterminate:focus{border-color:#2563eb}
.sel .form-check-input:focus-visible{outline:2px solid #2563eb;outline-offset:2px}
.sel tbody th{font-weight:600;--bs-table-color:#182030;--bs-table-hover-color:#182030;--bs-table-active-color:#182030}
.sel-email{--bs-table-color:#5f6b82;--bs-table-hover-color:#5f6b82;--bs-table-active-color:#5f6b82}
.sel .badge{--bs-badge-font-size:.75rem;--bs-badge-font-weight:700;--bs-badge-padding-x:10px;--bs-badge-padding-y:3px;display:inline;line-height:normal;background:#eef1f6;color:#4a5670}
.sel .badge.admin{background:#e7edff;color:#2b4acb}
JavaScript
// Row checkboxes drive .table-active on the row, the select-all state (checked / indeterminate) and the bulk bar.
(function () {
  var card = document.querySelector('.sel-card');
  if (!card) return;
  var all = card.querySelector('[data-sel-all]');
  var boxes = Array.prototype.slice.call(card.querySelectorAll('[data-sel-row]'));
  var bulk = card.querySelector('[data-sel-bulk]');
  var count = card.querySelector('[data-sel-count]');
  var status = card.querySelector('[data-sel-status]');
  function sync() {
    var n = boxes.filter(function (b) { return b.checked; }).length;
    boxes.forEach(function (b) { b.closest('tr').classList.toggle('table-active', b.checked); });
    bulk.hidden = n === 0;
    bulk.classList.toggle('d-flex', n > 0);
    count.textContent = n + ' selected';
    status.textContent = n ? n + ' of ' + boxes.length + ' selected' : 'No rows selected';
    all.checked = n === boxes.length;
    all.indeterminate = n > 0 && n < boxes.length;
  }
  boxes.forEach(function (b) { b.addEventListener('change', sync); });
  all.addEventListener('change', function () { boxes.forEach(function (b) { b.checked = all.checked; }); sync(); });
})();

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<section data-sel aria-labelledby="sel-title" class="relative w-full max-w-[720px] overflow-hidden rounded-2xl border border-[#e5eaf2] bg-white text-[#182030] shadow-[0_14px_32px_-18px_rgba(18,28,48,.16)]">
  <div class="flex min-h-[62px] flex-wrap items-center justify-between gap-2.5 border-b border-[#ecf0f6] px-5 py-[15px]">
    <h2 id="sel-title" class="text-base font-bold tracking-[-.01em]">Team members</h2>
    <div data-sel-bulk hidden class="items-center gap-2.5 [&:not([hidden])]:flex"><span data-sel-count class="text-[.85rem] font-semibold text-[#3d4a63]"></span><button type="button" class="cursor-pointer rounded-lg border bg-white px-[13px] py-[7px] text-[.8rem] font-semibold transition-colors duration-150 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb] motion-reduce:transition-none border-[#d7deea] text-[#3d4a63] hover:bg-[#f2f5fa]">Export</button><button type="button" class="cursor-pointer rounded-lg border bg-white px-[13px] py-[7px] text-[.8rem] font-semibold transition-colors duration-150 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb] motion-reduce:transition-none border-[#f1c8c8] text-[#b42323] hover:bg-[#fdf1f1]">Remove</button></div>
  </div>
  <p class="sr-only" role="status" data-sel-status></p>
  <div role="region" tabindex="0" aria-label="Team members, scrollable" class="relative overflow-x-auto focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[#2563eb]">
    <table aria-labelledby="sel-title" class="w-full border-collapse text-left text-[.88rem]">
      <thead class="bg-[#f8fafc] text-[.72rem] font-bold tracking-[.06em] text-[#5f6b82] uppercase [&>tr>*]:border-b [&>tr>*]:border-[#ecf0f6] [&>tr>*]:px-5 [&>tr>*]:py-[11px]"><tr><td class="w-px pr-0!"><input type="checkbox" data-sel-all aria-label="Select all team members" class="size-4 cursor-pointer accent-[#2563eb] block focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb]"></td><th scope="col">Name</th><th scope="col">Email</th><th scope="col">Role</th></tr></thead>
      <tbody class="text-[#34405a] [&>tr>*]:border-b [&>tr>*]:border-[#f2f5f9] [&>tr>*]:px-5 [&>tr>*]:py-3 [&>tr:last-child>*]:border-b-0">
        <tr class="hover:bg-[#f7fafd] has-checked:bg-[#eff4ff] has-checked:hover:bg-[#e8effd]"><td class="w-px pr-0!"><input type="checkbox" data-sel-row aria-label="Select Maja Lindgren" class="size-4 cursor-pointer accent-[#2563eb] block focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb]"></td><th scope="row" class="font-semibold text-[#182030]">Maja Lindgren</th><td class="text-[#5f6b82]">[email protected]</td><td><span class="rounded-full px-2.5 py-[3px] text-[.75rem] font-bold whitespace-nowrap bg-[#e7edff] text-[#2b4acb]">Admin</span></td></tr>
        <tr class="hover:bg-[#f7fafd] has-checked:bg-[#eff4ff] has-checked:hover:bg-[#e8effd]"><td class="w-px pr-0!"><input type="checkbox" data-sel-row aria-label="Select Diego Fuentes" class="size-4 cursor-pointer accent-[#2563eb] block focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb]"></td><th scope="row" class="font-semibold text-[#182030]">Diego Fuentes</th><td class="text-[#5f6b82]">[email protected]</td><td><span class="rounded-full px-2.5 py-[3px] text-[.75rem] font-bold whitespace-nowrap bg-[#eef1f6] text-[#4a5670]">Editor</span></td></tr>
        <tr class="hover:bg-[#f7fafd] has-checked:bg-[#eff4ff] has-checked:hover:bg-[#e8effd]"><td class="w-px pr-0!"><input type="checkbox" data-sel-row aria-label="Select Amara Obi" class="size-4 cursor-pointer accent-[#2563eb] block focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb]"></td><th scope="row" class="font-semibold text-[#182030]">Amara Obi</th><td class="text-[#5f6b82]">[email protected]</td><td><span class="rounded-full px-2.5 py-[3px] text-[.75rem] font-bold whitespace-nowrap bg-[#eef1f6] text-[#4a5670]">Editor</span></td></tr>
        <tr class="hover:bg-[#f7fafd] has-checked:bg-[#eff4ff] has-checked:hover:bg-[#e8effd]"><td class="w-px pr-0!"><input type="checkbox" data-sel-row aria-label="Select Jonas Weber" class="size-4 cursor-pointer accent-[#2563eb] block focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb]"></td><th scope="row" class="font-semibold text-[#182030]">Jonas Weber</th><td class="text-[#5f6b82]">[email protected]</td><td><span class="rounded-full px-2.5 py-[3px] text-[.75rem] font-bold whitespace-nowrap bg-[#eef1f6] text-[#4a5670]">Viewer</span></td></tr>
        <tr class="hover:bg-[#f7fafd] has-checked:bg-[#eff4ff] has-checked:hover:bg-[#e8effd]"><td class="w-px pr-0!"><input type="checkbox" data-sel-row aria-label="Select Yuki Tanaka" class="size-4 cursor-pointer accent-[#2563eb] block focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb]"></td><th scope="row" class="font-semibold text-[#182030]">Yuki Tanaka</th><td class="text-[#5f6b82]">[email protected]</td><td><span class="rounded-full px-2.5 py-[3px] text-[.75rem] font-bold whitespace-nowrap bg-[#eef1f6] text-[#4a5670]">Viewer</span></td></tr>
      </tbody>
    </table>
  </div>
</section>
JavaScript
// Rows highlight themselves with has-checked:; the script keeps select-all (checked / indeterminate) and the bulk bar in sync.
(function () {
  var card = document.querySelector('[data-sel]');
  if (!card) return;
  var all = card.querySelector('[data-sel-all]');
  var boxes = Array.prototype.slice.call(card.querySelectorAll('[data-sel-row]'));
  var bulk = card.querySelector('[data-sel-bulk]');
  var count = card.querySelector('[data-sel-count]');
  var status = card.querySelector('[data-sel-status]');
  function sync() {
    var n = boxes.filter(function (b) { return b.checked; }).length;
    bulk.hidden = n === 0;
    count.textContent = n + ' selected';
    status.textContent = n ? n + ' of ' + boxes.length + ' selected' : 'No rows selected';
    all.checked = n === boxes.length;
    all.indeterminate = n > 0 && n < boxes.length;
  }
  boxes.forEach(function (b) { b.addEventListener('change', sync); });
  all.addEventListener('change', function () { boxes.forEach(function (b) { b.checked = all.checked; }); sync(); });
})();

About this set

A team members table with a checkbox on every row, a select-all checkbox in the header and a bulk action bar that appears once something is selected. Ticking rows highlights them in light blue, the header checkbox turns indeterminate when only some rows are selected and fully checked when all are, and the bar in the card header shows how many are selected next to Export and Remove buttons. A small script keeps these in sync; the checkboxes are native inputs, so clicking, Space to toggle and form submission all work as usual. Each checkbox is named after its row, for example Select Maja Lindgren, rather than a repeated Select row, and a visually hidden status line announces the count, such as 2 of 5 selected, to screen reader users. Use it for user management, file lists and inboxes where people act on several rows at once. The Bootstrap version uses .form-check-input and marks selected rows with .table-active; the Tailwind version highlights rows with has-checked: so the row styling needs no script.

What’s included

  • Per-row checkboxes with names that include the row
  • Select-all that goes indeterminate on a partial selection
  • Bulk action bar that appears with the selected count
  • Selection count announced through a role="status" line
  • Bootstrap .table-active and Tailwind has-checked: highlighting

Accessibility

Every checkbox has an aria-label naming its person and the header checkbox is labelled Select all team members. The selection count is announced by a visually hidden role="status" element. Checkboxes, buttons and the scroll region show a 2px blue focus outline, and the button hover transition is removed under prefers-reduced-motion.

Customise it

Selection colours are .sel tbody tr.is-selected (#eff4ff) and accent-color #2563eb on the checkboxes. In Bootstrap set --bs-table-active-bg and the .form-check-input:checked colour; in Tailwind change has-checked:bg-[#eff4ff] and accent-[#2563eb].