Open demo

CSS code

HTML
<div class="tcard" data-bulk>
  <div class="bulk"><input type="checkbox" checked aria-label="Select all files" data-select-all> <span role="status" data-count>2 rows selected</span>
    <span class="sp"><button type="button">Export</button><button type="button">Move…</button><button class="bad" type="button">Delete</button></span></div>
  <div class="tscroll" role="region" tabindex="0" aria-label="Files, scrollable">
  <table class="tbl">
    <caption class="sr-only">Files. Each row has a download button and a menu of more actions.</caption>
    <thead><tr><th scope="col" style="width:36px"><span class="sr-only">Select</span></th><th scope="col">File</th><th scope="col">Owner</th><th scope="col" class="num">Size</th><th scope="col" style="width:82px"><span class="sr-only">Actions</span></th></tr></thead>
    <tbody>
      <tr><td><input type="checkbox" checked aria-label="Select brand-system-v4.fig"></td><td>brand-system-v4.fig</td><td>Rosa M.</td><td class="num">42.1 MB</td>
        <td><div class="acts">
          <button type="button" class="ib" aria-label="Download brand-system-v4.fig"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 4v11M7 11l5 5 5-5M5 20h14"/></svg></button>
          <button type="button" class="ib" popovertarget="rowmenu-1" aria-label="More actions for brand-system-v4.fig"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="5" r="1"/><circle cx="12" cy="12" r="1"/><circle cx="12" cy="19" r="1"/></svg></button>
          <div class="pop" id="rowmenu-1" popover data-row-menu><button type="button">Rename</button><button type="button">Duplicate</button><button type="button">Share…</button><hr><button class="bad" type="button">Delete</button></div>
        </div></td></tr>
      <tr><td><input type="checkbox" checked aria-label="Select 2026-annual-report.pdf"></td><td>2026-annual-report.pdf</td><td>Aigars K.</td><td class="num">3.8 MB</td>
        <td><div class="acts">
          <button type="button" class="ib" aria-label="Download 2026-annual-report.pdf"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 4v11M7 11l5 5 5-5M5 20h14"/></svg></button>
          <button type="button" class="ib" popovertarget="rowmenu-2" aria-label="More actions for 2026-annual-report.pdf"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="5" r="1"/><circle cx="12" cy="12" r="1"/><circle cx="12" cy="19" r="1"/></svg></button>
          <div class="pop" id="rowmenu-2" popover data-row-menu><button type="button">Rename</button><button type="button">Duplicate</button><button type="button">Share…</button><hr><button class="bad" type="button">Delete</button></div>
        </div></td></tr>
      <tr><td><input type="checkbox" aria-label="Select site-backup-aug.zip"></td><td>site-backup-aug.zip</td><td>Tomas P.</td><td class="num">118 MB</td>
        <td><div class="acts">
          <button type="button" class="ib" aria-label="Download site-backup-aug.zip"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 4v11M7 11l5 5 5-5M5 20h14"/></svg></button>
          <button type="button" class="ib" popovertarget="rowmenu-3" aria-label="More actions for site-backup-aug.zip"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="5" r="1"/><circle cx="12" cy="12" r="1"/><circle cx="12" cy="19" r="1"/></svg></button>
          <div class="pop" id="rowmenu-3" popover data-row-menu><button type="button">Rename</button><button type="button">Duplicate</button><button type="button">Share…</button><hr><button class="bad" type="button">Delete</button></div>
        </div></td></tr>
    </tbody>
  </table>
  </div>
</div>
CSS
/* Row actions: icon buttons that appear on row :hover and :focus-within (always shown on touch
   screens), a popover menu per row, and a bulk bar with a select-all checkbox above the table. */
.tcard{background:#fff;border:1px solid #e7e9f0;border-radius:14px;overflow:hidden}
.tscroll{overflow-x:auto;position:relative}
.tscroll:focus-visible{outline:2px solid #4f46e5;outline-offset:-2px}
.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}
.tbl{width:100%;border-collapse:collapse;font-size:.89rem;white-space:nowrap}
.tbl thead th{text-align:left;font-size:.74rem;letter-spacing:.06em;text-transform:uppercase;
  color:#5f6778;font-weight:700;padding:13px 16px;background:#fafbfc;border-bottom:1px solid #eceef3}
.tbl td{padding:13px 16px;border-bottom:1px solid #f2f4f7;color:#3c4048}
.tbl tbody tr:last-child td{border-bottom:0}
.tbl tbody tr:hover{background:#fafbfd}
.tbl .num{font-variant-numeric:tabular-nums;text-align:right}
.tbl thead th.num{text-align:right}
.tcard input[type=checkbox]{display:block;accent-color:#4f46e5;width:15px;height:15px}
.tcard input[type=checkbox]:focus-visible{outline:2px solid #4f46e5;outline-offset:2px}

.acts{display:flex;gap:4px;justify-content:flex-end;opacity:0;transition:opacity .15s}
tr:hover .acts,tr:focus-within .acts,tr:has(:popover-open) .acts{opacity:1}
@media(hover:none){.acts{opacity:1}}
.ib{width:30px;height:30px;border:0;background:none;border-radius:8px;
  cursor:pointer;color:#858d9c;display:grid;place-items:center}
.ib:hover{background:#f0f2f6;color:#101828}
.ib:focus-visible{outline:2px solid #4f46e5;outline-offset:2px}
.ib svg{display:block;width:15px;height:15px;stroke:currentColor;fill:none;stroke-width:2}
.pop{inset:auto;margin:0;background:#fff;border:1px solid #e4e7ec;color:#344054;
  border-radius:11px;padding:6px;min-width:180px;box-shadow:0 16px 34px -18px rgba(16,24,40,.32)}
.pop button{display:flex;align-items:center;gap:10px;width:100%;text-align:left;border:0;background:none;
  padding:8px 11px;border-radius:8px;font:inherit;font-size:.85rem;color:#344054;cursor:pointer}
.pop button:hover{background:#f5f6fa}
.pop button:focus-visible{outline:2px solid #4f46e5;outline-offset:-2px}
.pop button.bad{color:#dc2626}
.pop hr{border:0;border-top:1px solid #f0f2f6;margin:5px 0}
.bulk{display:flex;align-items:center;gap:14px;padding:11px 16px;background:#eef2ff;
  border-bottom:1px solid #dfe3f7;font-size:.86rem;color:#3730a3;font-weight:600;flex-wrap:wrap}
.bulk .sp{margin-left:auto;display:flex;gap:8px;flex-wrap:wrap}
.bulk button{font:inherit;font-size:.82rem;font-weight:700;padding:7px 14px;border-radius:8px;
  border:1px solid #c7cdf5;background:#fff;color:#3730a3;cursor:pointer}
.bulk button:hover{background:#f5f6ff}
.bulk button:focus-visible{outline:2px solid #4f46e5;outline-offset:2px}
.bulk button.bad{color:#b91c1c;border-color:#f3c9c9}
@media(prefers-reduced-motion:reduce){.acts{transition:none}}
JavaScript
// Row actions. 1) Selection: the header checkbox is checked, mixed or clear to match the rows,
// and the bulk bar count follows. 2) Row menus use the popover attribute, so they open in the
// top layer (never clipped by the scrolling table), close on Escape or an outside click, and
// return focus to their button. The script only places each menu under its button.
document.querySelectorAll('[data-bulk]').forEach(card => {
  const all = card.querySelector('[data-select-all]');
  const boxes = [...card.querySelectorAll('tbody input[type=checkbox]')];
  const count = card.querySelector('[data-count]');
  const sync = () => {
    const n = boxes.filter(b => b.checked).length;
    all.checked = n === boxes.length;
    all.indeterminate = n > 0 && n < boxes.length;
    count.textContent = `${n} row${n === 1 ? '' : 's'} selected`;
  };
  all.addEventListener('change', () => { boxes.forEach(b => { b.checked = all.checked; }); sync(); });
  boxes.forEach(b => b.addEventListener('change', sync));
  sync();
});

document.querySelectorAll('[data-row-menu]').forEach(menu => {
  const btn = document.querySelector(`[popovertarget="${menu.id}"]`);
  menu.addEventListener('toggle', e => {
    if (e.newState !== 'open') return;
    const r = btn.getBoundingClientRect();
    const below = r.bottom + 5 + menu.offsetHeight <= innerHeight;
    menu.style.top = `${below ? r.bottom + 5 : r.top - 5 - menu.offsetHeight}px`;
    menu.style.left = `${Math.max(8, r.right - menu.offsetWidth)}px`;
  });
});
// A fixed menu would drift from its row on scroll, so any scroll closes open row menus.
addEventListener('scroll', () => {
  document.querySelectorAll('[data-row-menu]:popover-open').forEach(m => m.hidePopover());
}, { capture: true, passive: true });

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<div class="card tcard overflow-hidden" data-bulk>
  <div class="card-header bulk d-flex flex-wrap align-items-center"><input class="form-check-input" type="checkbox" checked aria-label="Select all files" data-select-all> <span role="status" data-count>2 rows selected</span>
    <span class="ms-auto d-flex flex-wrap gap-2"><button type="button" class="btn">Export</button><button type="button" class="btn">Move…</button><button class="btn btn-bad" type="button">Delete</button></span></div>
  <div class="table-responsive" role="region" tabindex="0" aria-label="Files, scrollable">
  <table class="table table-hover align-middle mb-0 tbl">
    <caption class="visually-hidden">Files. Each row has a download button and a menu of more actions.</caption>
    <thead><tr><th scope="col" style="width:36px"><span class="visually-hidden">Select</span></th><th scope="col">File</th><th scope="col">Owner</th><th scope="col" class="text-end">Size</th><th scope="col" style="width:82px"><span class="visually-hidden">Actions</span></th></tr></thead>
    <tbody>
      <tr><td><input class="form-check-input" type="checkbox" checked aria-label="Select brand-system-v4.fig"></td><td>brand-system-v4.fig</td><td>Rosa M.</td><td class="text-end num">42.1 MB</td>
        <td><div class="acts d-flex justify-content-end gap-1">
          <button type="button" class="btn btn-icon" aria-label="Download brand-system-v4.fig"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 4v11M7 11l5 5 5-5M5 20h14"/></svg></button>
          <button type="button" class="btn btn-icon" popovertarget="rowmenu-1" aria-label="More actions for brand-system-v4.fig"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="5" r="1"/><circle cx="12" cy="12" r="1"/><circle cx="12" cy="19" r="1"/></svg></button>
          <div class="dropdown-menu row-menu" id="rowmenu-1" popover data-row-menu><button class="dropdown-item" type="button">Rename</button><button class="dropdown-item" type="button">Duplicate</button><button class="dropdown-item" type="button">Share…</button><hr class="dropdown-divider"><button class="dropdown-item text-danger" type="button">Delete</button></div>
        </div></td></tr>
      <tr><td><input class="form-check-input" type="checkbox" checked aria-label="Select 2026-annual-report.pdf"></td><td>2026-annual-report.pdf</td><td>Aigars K.</td><td class="text-end num">3.8 MB</td>
        <td><div class="acts d-flex justify-content-end gap-1">
          <button type="button" class="btn btn-icon" aria-label="Download 2026-annual-report.pdf"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 4v11M7 11l5 5 5-5M5 20h14"/></svg></button>
          <button type="button" class="btn btn-icon" popovertarget="rowmenu-2" aria-label="More actions for 2026-annual-report.pdf"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="5" r="1"/><circle cx="12" cy="12" r="1"/><circle cx="12" cy="19" r="1"/></svg></button>
          <div class="dropdown-menu row-menu" id="rowmenu-2" popover data-row-menu><button class="dropdown-item" type="button">Rename</button><button class="dropdown-item" type="button">Duplicate</button><button class="dropdown-item" type="button">Share…</button><hr class="dropdown-divider"><button class="dropdown-item text-danger" type="button">Delete</button></div>
        </div></td></tr>
      <tr><td><input class="form-check-input" type="checkbox" aria-label="Select site-backup-aug.zip"></td><td>site-backup-aug.zip</td><td>Tomas P.</td><td class="text-end num">118 MB</td>
        <td><div class="acts d-flex justify-content-end gap-1">
          <button type="button" class="btn btn-icon" aria-label="Download site-backup-aug.zip"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 4v11M7 11l5 5 5-5M5 20h14"/></svg></button>
          <button type="button" class="btn btn-icon" popovertarget="rowmenu-3" aria-label="More actions for site-backup-aug.zip"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="5" r="1"/><circle cx="12" cy="12" r="1"/><circle cx="12" cy="19" r="1"/></svg></button>
          <div class="dropdown-menu row-menu" id="rowmenu-3" popover data-row-menu><button class="dropdown-item" type="button">Rename</button><button class="dropdown-item" type="button">Duplicate</button><button class="dropdown-item" type="button">Share…</button><hr class="dropdown-divider"><button class="dropdown-item text-danger" type="button">Delete</button></div>
        </div></td></tr>
    </tbody>
  </table>
  </div>
</div>
CSS
/* Bootstrap .card + .table-hover + .form-check-input, with .dropdown-menu markup for the row menus.
   The menus are opened with the popover attribute instead of Bootstrap's dropdown JavaScript, so they
   sit in the top layer and a .table-responsive wrapper cannot clip them. */
.tcard{--bs-card-border-color:#e7e9f0;--bs-card-border-radius:14px;--bs-card-bg:#fff;
  --bs-card-cap-bg:#eef2ff;--bs-card-cap-padding-x:16px;--bs-card-cap-padding-y:11px}
.tcard .table-responsive:focus-visible{outline:2px solid #4f46e5;outline-offset:-2px}
.tbl{--bs-table-color:#3c4048;--bs-table-bg:#fff;--bs-table-border-color:#f2f4f7;
  --bs-table-hover-color:#3c4048;--bs-table-hover-bg:#fafbfd;font-size:.89rem;white-space:nowrap}
.tbl > :not(caption) > * > *{padding:13px 16px}
.tbl > tbody > tr:last-child > *{border-bottom-width:0}
.tbl > thead > tr > th{font-size:.74rem;letter-spacing:.06em;text-transform:uppercase;color:#5f6778;
  --bs-table-bg:#fafbfc;border-bottom-color:#eceef3}
.tbl .num{font-variant-numeric:tabular-nums}
.tcard .form-check-input{display:block;width:15px;height:15px;margin:0;border-color:#7c8aa0;
  --bs-form-check-bg:#fff}
.tcard .form-check-input:checked,.tcard .form-check-input:indeterminate{background-color:#4f46e5;border-color:#4f46e5}
.tcard .form-check-input:focus{box-shadow:none;border-color:#4f46e5}
.tcard .form-check-input:focus-visible{outline:2px solid #4f46e5;outline-offset:2px}
.acts{opacity:0;transition:opacity .15s}
tr:hover .acts,tr:focus-within .acts,tr:has(:popover-open) .acts{opacity:1}
@media(hover:none){.acts{opacity:1}}
.btn-icon{--bs-btn-padding-x:0;--bs-btn-padding-y:0;--bs-btn-border-width:0;--bs-btn-border-radius:8px;
  --bs-btn-color:#858d9c;--bs-btn-hover-color:#101828;--bs-btn-hover-bg:#f0f2f6;--bs-btn-active-bg:#e4e7ec;
  --bs-btn-active-color:#101828;--bs-btn-focus-box-shadow:0 0 0 2px #fff,0 0 0 4px #4f46e5;
  width:30px;height:30px;display:grid;place-items:center}
.btn-icon svg{display:block;width:15px;height:15px;stroke:currentColor;fill:none;stroke-width:2}
.row-menu{--bs-dropdown-min-width:180px;--bs-dropdown-padding-x:6px;--bs-dropdown-padding-y:6px;
  --bs-dropdown-font-size:.85rem;--bs-dropdown-border-color:#e4e7ec;--bs-dropdown-border-radius:11px;
  --bs-dropdown-link-color:#344054;--bs-dropdown-link-hover-bg:#f5f6fa;--bs-dropdown-link-active-bg:#eef2ff;
  --bs-dropdown-link-active-color:#3730a3;--bs-dropdown-item-padding-x:11px;--bs-dropdown-item-padding-y:8px;
  --bs-dropdown-item-border-radius:8px;--bs-dropdown-divider-bg:#f0f2f6;--bs-dropdown-divider-margin-y:5px;
  --bs-dropdown-box-shadow:0 16px 34px -18px rgba(16,24,40,.32);
  position:fixed;inset:auto;margin:0;box-shadow:var(--bs-dropdown-box-shadow)}
.row-menu:popover-open{display:block}
.row-menu .dropdown-item:focus-visible{outline:2px solid #4f46e5;outline-offset:-2px}
.row-menu .dropdown-item.text-danger{--bs-danger-rgb:220,38,38}
.bulk{gap:14px;font-size:.86rem;color:#3730a3;font-weight:600;border-bottom-color:#dfe3f7}
.bulk .btn{--bs-btn-padding-x:14px;--bs-btn-padding-y:7px;--bs-btn-font-size:.82rem;--bs-btn-font-weight:700;
  --bs-btn-line-height:normal;--bs-btn-border-radius:8px;--bs-btn-color:#3730a3;--bs-btn-bg:#fff;
  --bs-btn-border-color:#c7cdf5;--bs-btn-hover-color:#3730a3;--bs-btn-hover-bg:#f5f6ff;
  --bs-btn-hover-border-color:#c7cdf5;--bs-btn-active-bg:#e0e7ff;--bs-btn-active-border-color:#c7cdf5;
  --bs-btn-focus-box-shadow:0 0 0 2px #fff,0 0 0 4px #4f46e5}
.bulk .btn-bad{--bs-btn-color:#b91c1c;--bs-btn-hover-color:#b91c1c;--bs-btn-border-color:#f3c9c9;--bs-btn-hover-border-color:#f3c9c9}
@media(prefers-reduced-motion:reduce){.acts{transition:none}}
JavaScript
// Row actions. 1) Selection: the header checkbox is checked, mixed or clear to match the rows,
// and the bulk bar count follows. 2) Row menus use the popover attribute, so they open in the
// top layer (never clipped by the scrolling table), close on Escape or an outside click, and
// return focus to their button. The script only places each menu under its button.
document.querySelectorAll('[data-bulk]').forEach(card => {
  const all = card.querySelector('[data-select-all]');
  const boxes = [...card.querySelectorAll('tbody input[type=checkbox]')];
  const count = card.querySelector('[data-count]');
  const sync = () => {
    const n = boxes.filter(b => b.checked).length;
    all.checked = n === boxes.length;
    all.indeterminate = n > 0 && n < boxes.length;
    count.textContent = `${n} row${n === 1 ? '' : 's'} selected`;
  };
  all.addEventListener('change', () => { boxes.forEach(b => { b.checked = all.checked; }); sync(); });
  boxes.forEach(b => b.addEventListener('change', sync));
  sync();
});

document.querySelectorAll('[data-row-menu]').forEach(menu => {
  const btn = document.querySelector(`[popovertarget="${menu.id}"]`);
  menu.addEventListener('toggle', e => {
    if (e.newState !== 'open') return;
    const r = btn.getBoundingClientRect();
    const below = r.bottom + 5 + menu.offsetHeight <= innerHeight;
    menu.style.top = `${below ? r.bottom + 5 : r.top - 5 - menu.offsetHeight}px`;
    menu.style.left = `${Math.max(8, r.right - menu.offsetWidth)}px`;
  });
});
// A fixed menu would drift from its row on scroll, so any scroll closes open row menus.
addEventListener('scroll', () => {
  document.querySelectorAll('[data-row-menu]:popover-open').forEach(m => m.hidePopover());
}, { capture: true, passive: true });

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div class="overflow-hidden rounded-[14px] border border-[#e7e9f0] bg-white" data-bulk>
  <div class="flex flex-wrap items-center gap-3.5 border-b border-[#dfe3f7] bg-[#eef2ff] px-4 py-[11px] text-[.86rem] font-semibold text-[#3730a3]"><input class="block size-[15px] accent-[#4f46e5] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5]" type="checkbox" checked aria-label="Select all files" data-select-all> <span role="status" data-count>2 rows selected</span>
    <span class="ml-auto flex flex-wrap gap-2"><button type="button" class="cursor-pointer rounded-lg border bg-white px-3.5 py-[7px] text-[.82rem] font-bold hover:bg-[#f5f6ff] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5] border-[#c7cdf5] text-[#3730a3]">Export</button><button type="button" class="cursor-pointer rounded-lg border bg-white px-3.5 py-[7px] text-[.82rem] font-bold hover:bg-[#f5f6ff] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5] border-[#c7cdf5] text-[#3730a3]">Move…</button><button class="cursor-pointer rounded-lg border bg-white px-3.5 py-[7px] text-[.82rem] font-bold hover:bg-[#f5f6ff] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5] border-[#f3c9c9] text-[#b91c1c]" type="button">Delete</button></span></div>
  <div class="relative overflow-x-auto focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[#4f46e5]" role="region" tabindex="0" aria-label="Files, scrollable">
  <table class="w-full border-collapse text-[.89rem] whitespace-nowrap [&_td]:border-b [&_td]:border-[#f2f4f7] [&_td]:px-4 [&_td]:py-[13px] [&_td]:text-[#3c4048] [&_tbody_tr:last-child_td]:border-b-0">
    <caption class="sr-only">Files. Each row has a download button and a menu of more actions.</caption>
    <thead><tr><th scope="col" class="border-b border-[#eceef3] bg-[#fafbfc] px-4 py-[13px] text-[.74rem] font-bold tracking-[.06em] text-[#5f6778] uppercase w-9"><span class="sr-only">Select</span></th><th scope="col" class="border-b border-[#eceef3] bg-[#fafbfc] px-4 py-[13px] text-[.74rem] font-bold tracking-[.06em] text-[#5f6778] uppercase text-left">File</th><th scope="col" class="border-b border-[#eceef3] bg-[#fafbfc] px-4 py-[13px] text-[.74rem] font-bold tracking-[.06em] text-[#5f6778] uppercase text-left">Owner</th><th scope="col" class="border-b border-[#eceef3] bg-[#fafbfc] px-4 py-[13px] text-[.74rem] font-bold tracking-[.06em] text-[#5f6778] uppercase text-right">Size</th><th scope="col" class="border-b border-[#eceef3] bg-[#fafbfc] px-4 py-[13px] text-[.74rem] font-bold tracking-[.06em] text-[#5f6778] uppercase w-[82px]"><span class="sr-only">Actions</span></th></tr></thead>
    <tbody>
      <tr class="group hover:bg-[#fafbfd]"><td><input class="block size-[15px] accent-[#4f46e5] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5]" type="checkbox" checked aria-label="Select brand-system-v4.fig"></td><td>brand-system-v4.fig</td><td>Rosa M.</td><td class="text-right tabular-nums">42.1 MB</td>
        <td><div class="flex justify-end gap-1 opacity-0 transition-opacity duration-150 group-hover:opacity-100 group-focus-within:opacity-100 group-has-[:popover-open]:opacity-100 motion-reduce:transition-none [@media(hover:none)]:opacity-100">
          <button type="button" class="grid size-[30px] cursor-pointer place-items-center rounded-lg text-[#858d9c] hover:bg-[#f0f2f6] hover:text-[#101828] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5] [&>svg]:size-[15px] [&>svg]:fill-none [&>svg]:stroke-current [&>svg]:stroke-2" aria-label="Download brand-system-v4.fig"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 4v11M7 11l5 5 5-5M5 20h14"/></svg></button>
          <button type="button" class="grid size-[30px] cursor-pointer place-items-center rounded-lg text-[#858d9c] hover:bg-[#f0f2f6] hover:text-[#101828] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5] [&>svg]:size-[15px] [&>svg]:fill-none [&>svg]:stroke-current [&>svg]:stroke-2" popovertarget="rowmenu-1" aria-label="More actions for brand-system-v4.fig"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="5" r="1"/><circle cx="12" cy="12" r="1"/><circle cx="12" cy="19" r="1"/></svg></button>
          <div class="inset-auto m-0 min-w-[180px] rounded-[11px] border border-[#e4e7ec] bg-white p-1.5 text-[#344054] shadow-[0_16px_34px_-18px_rgba(16,24,40,.32)]" id="rowmenu-1" popover data-row-menu><button class="flex w-full cursor-pointer items-center rounded-lg px-[11px] py-2 text-left text-[.85rem] hover:bg-[#f5f6fa] focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[#4f46e5]" type="button">Rename</button><button class="flex w-full cursor-pointer items-center rounded-lg px-[11px] py-2 text-left text-[.85rem] hover:bg-[#f5f6fa] focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[#4f46e5]" type="button">Duplicate</button><button class="flex w-full cursor-pointer items-center rounded-lg px-[11px] py-2 text-left text-[.85rem] hover:bg-[#f5f6fa] focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[#4f46e5]" type="button">Share…</button><hr class="my-[5px] border-[#f0f2f6]"><button class="flex w-full cursor-pointer items-center rounded-lg px-[11px] py-2 text-left text-[.85rem] hover:bg-[#f5f6fa] focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[#4f46e5] text-[#dc2626]" type="button">Delete</button></div>
        </div></td></tr>
      <tr class="group hover:bg-[#fafbfd]"><td><input class="block size-[15px] accent-[#4f46e5] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5]" type="checkbox" checked aria-label="Select 2026-annual-report.pdf"></td><td>2026-annual-report.pdf</td><td>Aigars K.</td><td class="text-right tabular-nums">3.8 MB</td>
        <td><div class="flex justify-end gap-1 opacity-0 transition-opacity duration-150 group-hover:opacity-100 group-focus-within:opacity-100 group-has-[:popover-open]:opacity-100 motion-reduce:transition-none [@media(hover:none)]:opacity-100">
          <button type="button" class="grid size-[30px] cursor-pointer place-items-center rounded-lg text-[#858d9c] hover:bg-[#f0f2f6] hover:text-[#101828] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5] [&>svg]:size-[15px] [&>svg]:fill-none [&>svg]:stroke-current [&>svg]:stroke-2" aria-label="Download 2026-annual-report.pdf"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 4v11M7 11l5 5 5-5M5 20h14"/></svg></button>
          <button type="button" class="grid size-[30px] cursor-pointer place-items-center rounded-lg text-[#858d9c] hover:bg-[#f0f2f6] hover:text-[#101828] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5] [&>svg]:size-[15px] [&>svg]:fill-none [&>svg]:stroke-current [&>svg]:stroke-2" popovertarget="rowmenu-2" aria-label="More actions for 2026-annual-report.pdf"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="5" r="1"/><circle cx="12" cy="12" r="1"/><circle cx="12" cy="19" r="1"/></svg></button>
          <div class="inset-auto m-0 min-w-[180px] rounded-[11px] border border-[#e4e7ec] bg-white p-1.5 text-[#344054] shadow-[0_16px_34px_-18px_rgba(16,24,40,.32)]" id="rowmenu-2" popover data-row-menu><button class="flex w-full cursor-pointer items-center rounded-lg px-[11px] py-2 text-left text-[.85rem] hover:bg-[#f5f6fa] focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[#4f46e5]" type="button">Rename</button><button class="flex w-full cursor-pointer items-center rounded-lg px-[11px] py-2 text-left text-[.85rem] hover:bg-[#f5f6fa] focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[#4f46e5]" type="button">Duplicate</button><button class="flex w-full cursor-pointer items-center rounded-lg px-[11px] py-2 text-left text-[.85rem] hover:bg-[#f5f6fa] focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[#4f46e5]" type="button">Share…</button><hr class="my-[5px] border-[#f0f2f6]"><button class="flex w-full cursor-pointer items-center rounded-lg px-[11px] py-2 text-left text-[.85rem] hover:bg-[#f5f6fa] focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[#4f46e5] text-[#dc2626]" type="button">Delete</button></div>
        </div></td></tr>
      <tr class="group hover:bg-[#fafbfd]"><td><input class="block size-[15px] accent-[#4f46e5] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5]" type="checkbox" aria-label="Select site-backup-aug.zip"></td><td>site-backup-aug.zip</td><td>Tomas P.</td><td class="text-right tabular-nums">118 MB</td>
        <td><div class="flex justify-end gap-1 opacity-0 transition-opacity duration-150 group-hover:opacity-100 group-focus-within:opacity-100 group-has-[:popover-open]:opacity-100 motion-reduce:transition-none [@media(hover:none)]:opacity-100">
          <button type="button" class="grid size-[30px] cursor-pointer place-items-center rounded-lg text-[#858d9c] hover:bg-[#f0f2f6] hover:text-[#101828] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5] [&>svg]:size-[15px] [&>svg]:fill-none [&>svg]:stroke-current [&>svg]:stroke-2" aria-label="Download site-backup-aug.zip"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 4v11M7 11l5 5 5-5M5 20h14"/></svg></button>
          <button type="button" class="grid size-[30px] cursor-pointer place-items-center rounded-lg text-[#858d9c] hover:bg-[#f0f2f6] hover:text-[#101828] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5] [&>svg]:size-[15px] [&>svg]:fill-none [&>svg]:stroke-current [&>svg]:stroke-2" popovertarget="rowmenu-3" aria-label="More actions for site-backup-aug.zip"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="5" r="1"/><circle cx="12" cy="12" r="1"/><circle cx="12" cy="19" r="1"/></svg></button>
          <div class="inset-auto m-0 min-w-[180px] rounded-[11px] border border-[#e4e7ec] bg-white p-1.5 text-[#344054] shadow-[0_16px_34px_-18px_rgba(16,24,40,.32)]" id="rowmenu-3" popover data-row-menu><button class="flex w-full cursor-pointer items-center rounded-lg px-[11px] py-2 text-left text-[.85rem] hover:bg-[#f5f6fa] focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[#4f46e5]" type="button">Rename</button><button class="flex w-full cursor-pointer items-center rounded-lg px-[11px] py-2 text-left text-[.85rem] hover:bg-[#f5f6fa] focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[#4f46e5]" type="button">Duplicate</button><button class="flex w-full cursor-pointer items-center rounded-lg px-[11px] py-2 text-left text-[.85rem] hover:bg-[#f5f6fa] focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[#4f46e5]" type="button">Share…</button><hr class="my-[5px] border-[#f0f2f6]"><button class="flex w-full cursor-pointer items-center rounded-lg px-[11px] py-2 text-left text-[.85rem] hover:bg-[#f5f6fa] focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[#4f46e5] text-[#dc2626]" type="button">Delete</button></div>
        </div></td></tr>
    </tbody>
  </table>
  </div>
</div>
JavaScript
// Row actions. 1) Selection: the header checkbox is checked, mixed or clear to match the rows,
// and the bulk bar count follows. 2) Row menus use the popover attribute, so they open in the
// top layer (never clipped by the scrolling table), close on Escape or an outside click, and
// return focus to their button. The script only places each menu under its button.
document.querySelectorAll('[data-bulk]').forEach(card => {
  const all = card.querySelector('[data-select-all]');
  const boxes = [...card.querySelectorAll('tbody input[type=checkbox]')];
  const count = card.querySelector('[data-count]');
  const sync = () => {
    const n = boxes.filter(b => b.checked).length;
    all.checked = n === boxes.length;
    all.indeterminate = n > 0 && n < boxes.length;
    count.textContent = `${n} row${n === 1 ? '' : 's'} selected`;
  };
  all.addEventListener('change', () => { boxes.forEach(b => { b.checked = all.checked; }); sync(); });
  boxes.forEach(b => b.addEventListener('change', sync));
  sync();
});

document.querySelectorAll('[data-row-menu]').forEach(menu => {
  const btn = document.querySelector(`[popovertarget="${menu.id}"]`);
  menu.addEventListener('toggle', e => {
    if (e.newState !== 'open') return;
    const r = btn.getBoundingClientRect();
    const below = r.bottom + 5 + menu.offsetHeight <= innerHeight;
    menu.style.top = `${below ? r.bottom + 5 : r.top - 5 - menu.offsetHeight}px`;
    menu.style.left = `${Math.max(8, r.right - menu.offsetWidth)}px`;
  });
});
// A fixed menu would drift from its row on scroll, so any scroll closes open row menus.
addEventListener('scroll', () => {
  document.querySelectorAll('[data-row-menu]:popover-open').forEach(m => m.hidePopover());
}, { capture: true, passive: true });

About this set

A file list with per-row actions and a bulk action bar. Each row ends with a download button and a more actions button that opens a small menu with Rename, Duplicate, Share and Delete. The two icon buttons fade in when the row is hovered or when anything in it has keyboard focus, stay visible while the row’s menu is open, and are always visible on touch screens that cannot hover. Above the table, a tinted bar holds a select-all checkbox, a live count such as 2 rows selected, and Export, Move and Delete buttons. The select-all box shows checked, cleared or the mixed state to match the row checkboxes, and clicking it selects or clears them all. The menus use the HTML popover attribute, so they open in the top layer and are never clipped by the scrolling table, close on Escape or an outside click, and return focus to their button. A few lines of script place each menu under its button and close it if the page scrolls. The Bootstrap version styles the menus with .dropdown-menu and .dropdown-item without loading Bootstrap’s JavaScript.

What’s included

  • Row actions revealed on hover and on keyboard focus
  • Always visible on touch screens
  • Popover menus that a scrolling table cannot clip
  • Select-all checkbox with a mixed state
  • Live selected-row count in the bulk bar

Accessibility

Every checkbox and icon button has an accessible name that includes the file name, the empty header cells have hidden text, and the selected count is a role="status" region. Menus open with the native popover behaviour, so Escape closes them and focus returns to the button. All controls show a 2px indigo focus outline, icon colours reach 3:1, and the fade is removed under prefers-reduced-motion.

Customise it

The accent is #4f46e5 on the checkboxes, focus rings and bulk bar text; the bulk bar background is #eef2ff. Menu width, radius and shadow are on .pop (plain CSS), the --bs-dropdown-* variables on .row-menu (Bootstrap) or the classes on each popover (Tailwind).