Open demo

CSS code

HTML
<h2 class="tb-title" id="tb-title">Data table toolbar</h2>
<p class="tb-sub">Try the search, the density toggle and the columns menu. The chips below the bar are the filters already applied — each removable on its own.</p>
<section class="tb" data-tb aria-labelledby="tb-title">
  <div class="tb-bar">
    <label class="tb-search">
      <svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="11" cy="11" r="6"/><path d="M20 20l-4.3-4.3"/></svg>
      <input type="search" data-tb-search placeholder="Search invoices…" aria-label="Search invoices">
    </label>
    <details class="tb-menu" data-tb-menu>
      <summary class="tb-tool"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 6h16M7 12h10M10 18h4"/></svg> Columns</summary>
      <div class="tb-pop" role="group" aria-label="Visible columns">
        <label><input type="checkbox" data-col="0" checked> Client</label>
        <label><input type="checkbox" data-col="1" checked> Status</label>
        <label><input type="checkbox" data-col="2" checked> Issued</label>
        <label><input type="checkbox" data-col="3"> Due date</label>
        <label><input type="checkbox" data-col="4" checked> Amount</label>
      </div>
    </details>
    <button class="tb-tool" type="button" data-tb-density aria-pressed="false"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 6h16M4 12h16M4 18h16"/></svg> Compact</button>
    <button class="tb-tool" type="button"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 4v11M7 11l5 5 5-5M5 20h14"/></svg> Export</button>
  </div>
  <div class="tb-chips" data-tb-chips>
    <span class="tb-chip">Status: Paid <button type="button" data-tb-remove aria-label="Remove filter: Status is Paid"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg></button></span>
    <span class="tb-chip">Last 30 days <button type="button" data-tb-remove aria-label="Remove filter: Last 30 days"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg></button></span>
    <button class="tb-clear" type="button" data-tb-clear>Clear all</button>
  </div>
  <p class="sr-only" role="status" data-tb-status></p>
  <div class="tb-scroll" role="region" tabindex="0" aria-label="Invoices, scrollable">
    <table class="tb-table" data-tb-table aria-labelledby="tb-title">
      <thead><tr><th scope="col">Client</th><th scope="col">Status</th><th scope="col">Issued</th><th scope="col" hidden>Due date</th><th scope="col" class="num">Amount</th></tr></thead>
      <tbody>
        <tr><th scope="row">Meridian Ltd</th><td><span class="tb-pill ok">Paid</span></td><td>12 Aug 2026</td><td hidden>26 Aug 2026</td><td class="num">$584.40</td></tr>
        <tr><th scope="row">Halcyon Studio</th><td><span class="tb-pill ok">Paid</span></td><td>09 Aug 2026</td><td hidden>23 Aug 2026</td><td class="num">$1,220.00</td></tr>
        <tr><th scope="row">Northwind Co</th><td><span class="tb-pill warn">Pending</span></td><td>04 Aug 2026</td><td hidden>18 Aug 2026</td><td class="num">$318.75</td></tr>
        <tr><th scope="row">Fieldnote</th><td><span class="tb-pill ok">Paid</span></td><td>28 Jul 2026</td><td hidden>11 Aug 2026</td><td class="num">$2,940.00</td></tr>
      </tbody>
    </table>
  </div>
</section>
CSS
/* Data table toolbar: search, a columns menu (<details>), a density toggle (aria-pressed), export,
   and removable filter chips. The script filters rows, shows and hides columns, and toggles density. */
.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}
.tb-title{font-size:1.15rem;font-weight:700;letter-spacing:-.01em;margin:0 0 4px}
.tb-sub{color:#6b717d;font-size:.87rem;margin:0 0 22px;line-height:1.6}
.tb svg{display:block}
.tb :focus-visible{outline:2px solid #4f46e5;outline-offset:2px}
.tb{position:relative;background:#fff;color:#16181c;border:1px solid #e7e9f0;border-radius:14px;overflow:hidden}
.tb-bar{display:flex;align-items:center;gap:10px;padding:13px 14px;flex-wrap:wrap}
.tb-search{flex:1;min-width:180px;position:relative}
.tb-search svg{position:absolute;left:11px;top:50%;transform:translateY(-50%);width:15px;height:15px;stroke:#667085;fill:none;stroke-width:2}
.tb-search input{width:100%;padding:9px 12px 9px 34px;border:1px solid #8a94a6;border-radius:9px;font:inherit;font-size:.87rem;color:inherit;background:#fff}
.tb-search input:focus{outline:none;border-color:#4f46e5;box-shadow:0 0 0 3px rgba(79,70,229,.25)}
.tb-tool{display:inline-flex;align-items:center;gap:7px;border:1px solid #e4e7ec;background:#fff;border-radius:9px;padding:8px 13px;font:inherit;font-size:.85rem;font-weight:600;color:#475467;cursor:pointer}
.tb-tool:hover{background:#f7f8fa;color:#101828}
.tb-tool[aria-pressed="true"]{background:#eef2ff;border-color:#c7d2fe;color:#3730a3}
.tb-tool svg{width:14px;height:14px;stroke:currentColor;fill:none;stroke-width:2}
.tb-menu{position:relative}
.tb-menu>summary{list-style:none}
.tb-menu>summary::-webkit-details-marker{display:none}
.tb-pop{position:absolute;top:calc(100% + 6px);right:0;background:#fff;border:1px solid #e4e7ec;border-radius:11px;padding:7px;min-width:190px;z-index:5;box-shadow:0 16px 34px -18px rgba(16,24,40,.3)}
@media (max-width:480px){.tb-pop{right:auto;left:0}}
.tb-pop label{display:flex;align-items:center;gap:10px;padding:8px 10px;border-radius:8px;font-size:.86rem;color:#344054;cursor:pointer}
.tb-pop label:hover{background:#f5f6fa}
.tb-pop input{accent-color:#4f46e5}
.tb-chips{display:flex;gap:7px;padding:0 14px 13px;flex-wrap:wrap}
.tb-chips[hidden]{display:none}
.tb-chip{display:inline-flex;align-items:center;gap:8px;padding:5px 7px 5px 12px;border-radius:999px;font-size:.79rem;font-weight:600;background:#eef2ff;color:#3730a3}
.tb-chip button{border:0;background:rgba(55,48,163,.13);color:#3730a3;width:17px;height:17px;border-radius:50%;cursor:pointer;display:grid;place-items:center;padding:0}
.tb-chip button:hover{background:rgba(55,48,163,.26)}
.tb-chip svg{width:8px;height:8px;stroke:currentColor;fill:none;stroke-width:3.4}
.tb-clear{font:inherit;font-size:.79rem;font-weight:700;color:#667085;background:none;border:0;cursor:pointer;padding:5px 8px;border-radius:7px}
.tb-clear:hover{background:#f2f4f7;color:#101828}
.tb-scroll{position:relative;overflow-x:auto}
.tb .tb-scroll:focus-visible{outline-offset:-2px}
.tb-table{width:100%;border-collapse:collapse;font-size:.89rem}
.tb-table th,.tb-table td{text-align:left}
.tb-table thead th{font-size:.74rem;letter-spacing:.06em;text-transform:uppercase;color:#667085;font-weight:700;padding:13px 16px;background:#fafbfc;border-top:1px solid #eceef3;border-bottom:1px solid #eceef3;white-space:nowrap}
.tb-table tbody th,.tb-table td{padding:13px 16px;border-bottom:1px solid #f2f4f7;color:#3c4048;font-weight:400;white-space:nowrap}
.tb-table tbody tr:last-child>*{border-bottom:0}
.tb-table tbody tr:hover{background:#fafbfd}
.tb-table .num{font-variant-numeric:tabular-nums;text-align:right}
.tb-table[data-dense] tbody th,.tb-table[data-dense] td{padding:7px 16px}
.tb-table[data-dense] thead th{padding:8px 16px}
.tb-pill{display:inline-flex;align-items:center;gap:6px;font-size:.75rem;font-weight:700;padding:3px 10px;border-radius:999px}
.tb-pill.ok{background:#e8f8ee;color:#15803d}
.tb-pill.warn{background:#fff7e6;color:#b45309}
JavaScript
(function () {
  var root = document.querySelector('[data-tb]');
  if (!root) return;
  var table = root.querySelector('[data-tb-table]');
  var rows = Array.prototype.slice.call(table.tBodies[0].rows);
  var status = root.querySelector('[data-tb-status]');
  var chips = root.querySelector('[data-tb-chips]');
  var search = root.querySelector('[data-tb-search]');
  var menu = root.querySelector('[data-tb-menu]');

  // Search: hide rows whose text does not match, and announce the count.
  search.addEventListener('input', function () {
    var q = search.value.trim().toLowerCase(), shown = 0;
    rows.forEach(function (r) { var hit = r.textContent.toLowerCase().indexOf(q) > -1; r.hidden = !hit; if (hit) shown++; });
    status.textContent = shown + (shown === 1 ? ' invoice' : ' invoices') + ' shown';
  });

  // Columns menu: each checkbox shows or hides one column (header and cells).
  menu.querySelectorAll('input[data-col]').forEach(function (box) {
    box.addEventListener('change', function () {
      var i = Number(box.getAttribute('data-col'));
      Array.prototype.forEach.call(table.rows, function (r) { if (r.cells[i]) r.cells[i].hidden = !box.checked; });
    });
  });
  // Escape or a click outside closes the menu.
  menu.addEventListener('keydown', function (e) { if (e.key === 'Escape' && menu.open) { menu.open = false; menu.querySelector('summary').focus(); } });
  document.addEventListener('click', function (e) { if (menu.open && !menu.contains(e.target)) menu.open = false; });

  // Density: one attribute on the table, with aria-pressed kept in sync so the button reports its state.
  var density = root.querySelector('[data-tb-density]');
  density.addEventListener('click', function () {
    var on = density.getAttribute('aria-pressed') !== 'true';
    density.setAttribute('aria-pressed', String(on));
    table.toggleAttribute('data-dense', on);
  });

  // Filter chips: remove one, or all; focus moves to the next control so it is never lost.
  function afterRemove(next) {
    if (!chips.querySelector('[data-tb-remove]')) { chips.hidden = true; search.focus(); }
    else (next || chips.querySelector('button')).focus();
  }
  chips.addEventListener('click', function (e) {
    var btn = e.target.closest('[data-tb-remove]');
    if (btn) {
      var chip = btn.closest('span'), next = chip.nextElementSibling && chip.nextElementSibling.querySelector('button') || chip.nextElementSibling;
      chip.remove(); status.textContent = 'Filter removed'; afterRemove(next);
    } else if (e.target.closest('[data-tb-clear]')) {
      chips.querySelectorAll('[data-tb-remove]').forEach(function (b) { b.closest('span').remove(); });
      status.textContent = 'All filters cleared'; afterRemove();
    }
  });
})();

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS, Bootstrap 5.3.8 JS bundle

HTML
<h2 class="tb-title" id="tb-title">Data table toolbar</h2>
<p class="tb-sub">Try the search, the density toggle and the columns menu. The chips below the bar are the filters already applied — each removable on its own.</p>
<section class="tb card overflow-hidden" data-tb aria-labelledby="tb-title">
  <div class="tb-bar d-flex align-items-center flex-wrap">
    <div class="tb-search position-relative">
      <svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="11" cy="11" r="6"/><path d="M20 20l-4.3-4.3"/></svg>
      <input class="form-control" type="search" data-tb-search placeholder="Search invoices…" aria-label="Search invoices">
    </div>
    <div class="dropdown" data-tb-menu>
      <button class="btn tb-tool" type="button" data-bs-toggle="dropdown" data-bs-auto-close="outside" aria-expanded="false"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 6h16M7 12h10M10 18h4"/></svg> Columns</button>
      <div class="dropdown-menu dropdown-menu-end tb-pop" role="group" aria-label="Visible columns">
        <label class="dropdown-item"><input class="form-check-input" type="checkbox" data-col="0" checked> Client</label>
        <label class="dropdown-item"><input class="form-check-input" type="checkbox" data-col="1" checked> Status</label>
        <label class="dropdown-item"><input class="form-check-input" type="checkbox" data-col="2" checked> Issued</label>
        <label class="dropdown-item"><input class="form-check-input" type="checkbox" data-col="3"> Due date</label>
        <label class="dropdown-item"><input class="form-check-input" type="checkbox" data-col="4" checked> Amount</label>
      </div>
    </div>
    <button class="btn tb-tool" type="button" data-tb-density aria-pressed="false"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 6h16M4 12h16M4 18h16"/></svg> Compact</button>
    <button class="btn tb-tool" type="button"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 4v11M7 11l5 5 5-5M5 20h14"/></svg> Export</button>
  </div>
  <div class="tb-chips d-flex flex-wrap" data-tb-chips>
    <span class="tb-chip">Status: Paid <button type="button" data-tb-remove aria-label="Remove filter: Status is Paid"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg></button></span>
    <span class="tb-chip">Last 30 days <button type="button" data-tb-remove aria-label="Remove filter: Last 30 days"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg></button></span>
    <button class="btn tb-clear" type="button" data-tb-clear>Clear all</button>
  </div>
  <p class="visually-hidden" role="status" data-tb-status></p>
  <div class="tb-scroll table-responsive position-relative" role="region" tabindex="0" aria-label="Invoices, scrollable">
    <table class="table table-hover tb-table align-middle mb-0" data-tb-table aria-labelledby="tb-title">
      <thead><tr><th scope="col">Client</th><th scope="col">Status</th><th scope="col">Issued</th><th scope="col" hidden>Due date</th><th scope="col" class="num text-end">Amount</th></tr></thead>
      <tbody>
        <tr><th scope="row">Meridian Ltd</th><td><span class="badge rounded-pill ok">Paid</span></td><td>12 Aug 2026</td><td hidden>26 Aug 2026</td><td class="num text-end">$584.40</td></tr>
        <tr><th scope="row">Halcyon Studio</th><td><span class="badge rounded-pill ok">Paid</span></td><td>09 Aug 2026</td><td hidden>23 Aug 2026</td><td class="num text-end">$1,220.00</td></tr>
        <tr><th scope="row">Northwind Co</th><td><span class="badge rounded-pill warn">Pending</span></td><td>04 Aug 2026</td><td hidden>18 Aug 2026</td><td class="num text-end">$318.75</td></tr>
        <tr><th scope="row">Fieldnote</th><td><span class="badge rounded-pill ok">Paid</span></td><td>28 Jul 2026</td><td hidden>11 Aug 2026</td><td class="num text-end">$2,940.00</td></tr>
      </tbody>
    </table>
  </div>
</section>
CSS
/* Bootstrap toolbar over a .table: .form-control search, a .dropdown columns menu (auto-close="outside" so it stays open
   while you tick), a density .btn that switches .table-sm, and removable filter chips. */
.tb-title{font-size:1.15rem;font-weight:700;letter-spacing:-.01em;line-height:normal;margin:0 0 4px}
.tb-sub{color:#6b717d;font-size:.87rem;margin:0 0 22px;line-height:1.6}
.tb svg{display:block}
.tb{--bs-card-border-color:#e7e9f0;--bs-card-border-radius:14px;--bs-card-inner-border-radius:13px;color:#16181c}
.tb-bar{gap:10px;padding:13px 14px}
.tb-search{flex:1;min-width:180px}
.tb-search svg{position:absolute;left:11px;top:50%;transform:translateY(-50%);width:15px;height:15px;stroke:#667085;fill:none;stroke-width:2;pointer-events:none}
.tb-search .form-control{padding:9px 12px 9px 34px;border-color:#8a94a6;border-radius:9px;font-size:.87rem;line-height:normal}
.tb-search .form-control:focus{border-color:#4f46e5;box-shadow:0 0 0 3px rgba(79,70,229,.25)}
.tb .tb-tool{--bs-btn-font-size:.85rem;--bs-btn-font-weight:600;--bs-btn-line-height:normal;--bs-btn-padding-x:13px;--bs-btn-padding-y:8px;--bs-btn-border-radius:9px;
  --bs-btn-color:#475467;--bs-btn-bg:#fff;--bs-btn-border-color:#e4e7ec;--bs-btn-hover-color:#101828;--bs-btn-hover-bg:#f7f8fa;--bs-btn-hover-border-color:#e4e7ec;
  --bs-btn-active-color:#3730a3;--bs-btn-active-bg:#eef2ff;--bs-btn-active-border-color:#c7d2fe;--bs-btn-focus-box-shadow:0 0 0 2px #fff,0 0 0 4px #4f46e5;display:inline-flex;align-items:center;gap:7px}
.tb .tb-tool.show{color:#101828;background:#f7f8fa;border-color:#e4e7ec}
.tb-tool svg{width:14px;height:14px;stroke:currentColor;fill:none;stroke-width:2}
.tb-pop{--bs-dropdown-min-width:190px;--bs-dropdown-padding-x:7px;--bs-dropdown-padding-y:7px;--bs-dropdown-border-color:#e4e7ec;--bs-dropdown-border-radius:11px;
  --bs-dropdown-font-size:.86rem;--bs-dropdown-link-color:#344054;--bs-dropdown-link-hover-color:#344054;--bs-dropdown-link-hover-bg:#f5f6fa;--bs-dropdown-link-active-color:#344054;--bs-dropdown-link-active-bg:#f5f6fa;
  --bs-dropdown-item-padding-x:10px;--bs-dropdown-item-padding-y:8px;--bs-dropdown-item-border-radius:8px;--bs-dropdown-box-shadow:0 16px 34px -18px rgba(16,24,40,.3);box-shadow:var(--bs-dropdown-box-shadow)}
.tb-pop .dropdown-item{display:flex;align-items:center;gap:10px;cursor:pointer}
.tb-pop .form-check-input{margin:0;border-color:#7a8699}
.tb-pop .form-check-input:checked{background-color:#4f46e5;border-color:#4f46e5}
.tb-pop .form-check-input:focus{box-shadow:none}
.tb-pop .form-check-input:focus-visible{outline:2px solid #4f46e5;outline-offset:2px}
.tb-chips{gap:7px;padding:0 14px 13px}
.tb-chip{display:inline-flex;align-items:center;gap:8px;padding:5px 7px 5px 12px;border-radius:999px;font-size:.79rem;font-weight:600;background:#eef2ff;color:#3730a3}
.tb-chip button{border:0;background:rgba(55,48,163,.13);color:#3730a3;width:17px;height:17px;border-radius:50%;display:grid;place-items:center;padding:0}
.tb-chip button:hover{background:rgba(55,48,163,.26)}
.tb-chip svg{width:8px;height:8px;stroke:currentColor;fill:none;stroke-width:3.4}
.tb .tb-clear{--bs-btn-font-size:.79rem;--bs-btn-font-weight:700;--bs-btn-line-height:normal;--bs-btn-padding-x:8px;--bs-btn-padding-y:5px;--bs-btn-border-radius:7px;
  --bs-btn-color:#667085;--bs-btn-hover-color:#101828;--bs-btn-hover-bg:#f2f4f7;--bs-btn-border-color:transparent;--bs-btn-hover-border-color:transparent;--bs-btn-focus-box-shadow:0 0 0 2px #4f46e5}
.tb-chip button:focus-visible,.tb-scroll:focus-visible{outline:2px solid #4f46e5;outline-offset:2px}
.tb-scroll:focus-visible{outline-offset:-2px}
.tb-table{--bs-table-bg:#fff;--bs-table-color:#3c4048;--bs-table-border-color:#f2f4f7;--bs-table-hover-bg:#fafbfd;--bs-table-hover-color:#3c4048;font-size:.89rem}
.tb-table>:not(caption)>*>*{padding:13px 16px;white-space:nowrap}
.tb-table>thead>tr>th{--bs-table-bg:#fafbfc;--bs-table-color:#667085;font-size:.74rem;letter-spacing:.06em;text-transform:uppercase;font-weight:700;border-top:1px solid #eceef3;border-bottom-color:#eceef3}
.tb-table>tbody>tr>th{font-weight:400}
.tb-table>tbody>tr:last-child>*{border-bottom-width:0}
.tb-table.table-sm>tbody>tr>*{padding:7px 16px}
.tb-table.table-sm>thead>tr>*{padding:8px 16px}
.tb-table .num{font-variant-numeric:tabular-nums}
.tb-table .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}
.tb-table .badge.ok{background:#e8f8ee;color:#15803d}
.tb-table .badge.warn{background:#fff7e6;color:#b45309}
JavaScript
(function () {
  var root = document.querySelector('[data-tb]');
  if (!root) return;
  var table = root.querySelector('[data-tb-table]');
  var rows = Array.prototype.slice.call(table.tBodies[0].rows);
  var status = root.querySelector('[data-tb-status]');
  var chips = root.querySelector('[data-tb-chips]');
  var search = root.querySelector('[data-tb-search]');
  var menu = root.querySelector('[data-tb-menu]'); // Bootstrap's Dropdown plugin opens and closes it

  // Search: hide rows whose text does not match, and announce the count.
  search.addEventListener('input', function () {
    var q = search.value.trim().toLowerCase(), shown = 0;
    rows.forEach(function (r) { var hit = r.textContent.toLowerCase().indexOf(q) > -1; r.hidden = !hit; if (hit) shown++; });
    status.textContent = shown + (shown === 1 ? ' invoice' : ' invoices') + ' shown';
  });

  // Columns menu: each checkbox shows or hides one column (header and cells).
  menu.querySelectorAll('input[data-col]').forEach(function (box) {
    box.addEventListener('change', function () {
      var i = Number(box.getAttribute('data-col'));
      Array.prototype.forEach.call(table.rows, function (r) { if (r.cells[i]) r.cells[i].hidden = !box.checked; });
    });
  });

  // Density: Bootstrap's .table-sm on the table, with aria-pressed kept in sync so the button reports its state.
  var density = root.querySelector('[data-tb-density]');
  density.addEventListener('click', function () {
    var on = density.getAttribute('aria-pressed') !== 'true';
    density.setAttribute('aria-pressed', String(on));
    density.classList.toggle('active', on);
    table.classList.toggle('table-sm', on);
  });

  // Filter chips: remove one, or all; focus moves to the next control so it is never lost.
  function afterRemove(next) {
    if (!chips.querySelector('[data-tb-remove]')) { chips.hidden = true; search.focus(); }
    else (next || chips.querySelector('button')).focus();
  }
  chips.addEventListener('click', function (e) {
    var btn = e.target.closest('[data-tb-remove]');
    if (btn) {
      var chip = btn.closest('span'), next = chip.nextElementSibling && chip.nextElementSibling.querySelector('button') || chip.nextElementSibling;
      chip.remove(); status.textContent = 'Filter removed'; afterRemove(next);
    } else if (e.target.closest('[data-tb-clear]')) {
      chips.querySelectorAll('[data-tb-remove]').forEach(function (b) { b.closest('span').remove(); });
      status.textContent = 'All filters cleared'; afterRemove();
    }
  });
})();

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<h2 id="tb-title" class="mb-1 text-[1.15rem] font-bold tracking-[-.01em]">Data table toolbar</h2>
<p class="mb-[22px] text-[.87rem] leading-[1.6] text-[#6b717d]">Try the search, the density toggle and the columns menu. The chips below the bar are the filters already applied — each removable on its own.</p>
<section data-tb aria-labelledby="tb-title" class="relative overflow-hidden rounded-[14px] border border-[#e7e9f0] bg-white text-[#16181c]">
  <div class="flex flex-wrap items-center gap-2.5 px-3.5 py-[13px]">
    <label class="relative min-w-[180px] flex-1">
      <svg viewBox="0 0 24 24" aria-hidden="true" class="absolute top-1/2 left-[11px] size-[15px] -translate-y-1/2 fill-none stroke-[#667085] stroke-2"><circle cx="11" cy="11" r="6"/><path d="M20 20l-4.3-4.3"/></svg>
      <input type="search" data-tb-search placeholder="Search invoices…" aria-label="Search invoices" class="w-full rounded-[9px] border border-[#8a94a6] bg-white py-[9px] pr-3 pl-[34px] text-[.87rem] focus:border-[#4f46e5] focus:shadow-[0_0_0_3px_rgba(79,70,229,.25)] focus:outline-none">
    </label>
    <details data-tb-menu class="relative">
      <summary class="list-none [&::-webkit-details-marker]:hidden inline-flex cursor-pointer items-center gap-[7px] rounded-[9px] border border-[#e4e7ec] bg-white px-[13px] py-2 text-[.85rem] font-semibold text-[#475467] hover:bg-[#f7f8fa] hover:text-[#101828] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5] [&_svg]:size-3.5 [&_svg]:fill-none [&_svg]:stroke-current [&_svg]:stroke-2"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 6h16M7 12h10M10 18h4"/></svg> Columns</summary>
      <div role="group" aria-label="Visible columns" class="absolute top-[calc(100%+6px)] right-0 z-[5] max-[480px]:right-auto max-[480px]:left-0 min-w-[190px] rounded-[11px] border border-[#e4e7ec] bg-white p-[7px] shadow-[0_16px_34px_-18px_rgba(16,24,40,.3)]">
        <label class="flex cursor-pointer items-center gap-2.5 rounded-lg px-2.5 py-2 text-[.86rem] text-[#344054] hover:bg-[#f5f6fa]"><input type="checkbox" data-col="0" checked class="accent-[#4f46e5] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5]"> Client</label>
        <label class="flex cursor-pointer items-center gap-2.5 rounded-lg px-2.5 py-2 text-[.86rem] text-[#344054] hover:bg-[#f5f6fa]"><input type="checkbox" data-col="1" checked class="accent-[#4f46e5] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5]"> Status</label>
        <label class="flex cursor-pointer items-center gap-2.5 rounded-lg px-2.5 py-2 text-[.86rem] text-[#344054] hover:bg-[#f5f6fa]"><input type="checkbox" data-col="2" checked class="accent-[#4f46e5] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5]"> Issued</label>
        <label class="flex cursor-pointer items-center gap-2.5 rounded-lg px-2.5 py-2 text-[.86rem] text-[#344054] hover:bg-[#f5f6fa]"><input type="checkbox" data-col="3" class="accent-[#4f46e5] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5]"> Due date</label>
        <label class="flex cursor-pointer items-center gap-2.5 rounded-lg px-2.5 py-2 text-[.86rem] text-[#344054] hover:bg-[#f5f6fa]"><input type="checkbox" data-col="4" checked class="accent-[#4f46e5] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5]"> Amount</label>
      </div>
    </details>
    <button type="button" data-tb-density aria-pressed="false" class="inline-flex cursor-pointer items-center gap-[7px] rounded-[9px] border border-[#e4e7ec] bg-white px-[13px] py-2 text-[.85rem] font-semibold text-[#475467] hover:bg-[#f7f8fa] hover:text-[#101828] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5] [&_svg]:size-3.5 [&_svg]:fill-none [&_svg]:stroke-current [&_svg]:stroke-2 aria-pressed:border-[#c7d2fe] aria-pressed:bg-[#eef2ff] aria-pressed:text-[#3730a3]"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 6h16M4 12h16M4 18h16"/></svg> Compact</button>
    <button type="button" class="inline-flex cursor-pointer items-center gap-[7px] rounded-[9px] border border-[#e4e7ec] bg-white px-[13px] py-2 text-[.85rem] font-semibold text-[#475467] hover:bg-[#f7f8fa] hover:text-[#101828] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5] [&_svg]:size-3.5 [&_svg]:fill-none [&_svg]:stroke-current [&_svg]:stroke-2"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 4v11M7 11l5 5 5-5M5 20h14"/></svg> Export</button>
  </div>
  <div data-tb-chips class="flex flex-wrap gap-[7px] px-3.5 pb-[13px]">
    <span class="inline-flex items-center gap-2 rounded-full bg-[#eef2ff] py-[5px] pr-[7px] pl-3 text-[.79rem] font-semibold text-[#3730a3]">Status: Paid <button type="button" data-tb-remove aria-label="Remove filter: Status is Paid" class="grid size-[17px] cursor-pointer place-items-center rounded-full bg-[rgba(55,48,163,.13)] text-[#3730a3] hover:bg-[rgba(55,48,163,.26)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5] [&_svg]:size-2 [&_svg]:fill-none [&_svg]:stroke-current [&_svg]:stroke-[3.4]"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg></button></span>
    <span class="inline-flex items-center gap-2 rounded-full bg-[#eef2ff] py-[5px] pr-[7px] pl-3 text-[.79rem] font-semibold text-[#3730a3]">Last 30 days <button type="button" data-tb-remove aria-label="Remove filter: Last 30 days" class="grid size-[17px] cursor-pointer place-items-center rounded-full bg-[rgba(55,48,163,.13)] text-[#3730a3] hover:bg-[rgba(55,48,163,.26)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5] [&_svg]:size-2 [&_svg]:fill-none [&_svg]:stroke-current [&_svg]:stroke-[3.4]"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg></button></span>
    <button type="button" data-tb-clear class="cursor-pointer rounded-[7px] px-2 py-[5px] text-[.79rem] font-bold text-[#667085] hover:bg-[#f2f4f7] hover:text-[#101828] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5]">Clear all</button>
  </div>
  <p class="sr-only" role="status" data-tb-status></p>
  <div role="region" tabindex="0" aria-label="Invoices, scrollable" class="relative overflow-x-auto focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[#4f46e5]">
    <table data-tb-table aria-labelledby="tb-title" class="w-full border-collapse text-left text-[.89rem] whitespace-nowrap data-dense:[&_tbody_td]:py-[7px] data-dense:[&_tbody_th]:py-[7px] data-dense:[&_thead_th]:py-2">
      <thead class="bg-[#fafbfc] text-[.74rem] font-bold tracking-[.06em] text-[#667085] uppercase [&_th]:border-y [&_th]:border-[#eceef3] [&_th]:px-4 [&_th]:py-[13px]"><tr><th scope="col">Client</th><th scope="col">Status</th><th scope="col">Issued</th><th scope="col" hidden>Due date</th><th scope="col" class="text-right">Amount</th></tr></thead>
      <tbody class="text-[#3c4048] [&>tr>*]:border-b [&>tr>*]:border-[#f2f4f7] [&>tr>*]:px-4 [&>tr>*]:py-[13px] [&>tr>th]:font-normal [&>tr:last-child>*]:border-b-0">
        <tr class="hover:bg-[#fafbfd]"><th scope="row">Meridian Ltd</th><td><span class="inline-flex items-center rounded-full px-2.5 py-[3px] text-[.75rem] font-bold bg-[#e8f8ee] text-[#15803d]">Paid</span></td><td>12 Aug 2026</td><td hidden>26 Aug 2026</td><td class="text-right tabular-nums">$584.40</td></tr>
        <tr class="hover:bg-[#fafbfd]"><th scope="row">Halcyon Studio</th><td><span class="inline-flex items-center rounded-full px-2.5 py-[3px] text-[.75rem] font-bold bg-[#e8f8ee] text-[#15803d]">Paid</span></td><td>09 Aug 2026</td><td hidden>23 Aug 2026</td><td class="text-right tabular-nums">$1,220.00</td></tr>
        <tr class="hover:bg-[#fafbfd]"><th scope="row">Northwind Co</th><td><span class="inline-flex items-center rounded-full px-2.5 py-[3px] text-[.75rem] font-bold bg-[#fff7e6] text-[#b45309]">Pending</span></td><td>04 Aug 2026</td><td hidden>18 Aug 2026</td><td class="text-right tabular-nums">$318.75</td></tr>
        <tr class="hover:bg-[#fafbfd]"><th scope="row">Fieldnote</th><td><span class="inline-flex items-center rounded-full px-2.5 py-[3px] text-[.75rem] font-bold bg-[#e8f8ee] text-[#15803d]">Paid</span></td><td>28 Jul 2026</td><td hidden>11 Aug 2026</td><td class="text-right tabular-nums">$2,940.00</td></tr>
      </tbody>
    </table>
  </div>
</section>
JavaScript
(function () {
  var root = document.querySelector('[data-tb]');
  if (!root) return;
  var table = root.querySelector('[data-tb-table]');
  var rows = Array.prototype.slice.call(table.tBodies[0].rows);
  var status = root.querySelector('[data-tb-status]');
  var chips = root.querySelector('[data-tb-chips]');
  var search = root.querySelector('[data-tb-search]');
  var menu = root.querySelector('[data-tb-menu]');

  // Search: hide rows whose text does not match, and announce the count.
  search.addEventListener('input', function () {
    var q = search.value.trim().toLowerCase(), shown = 0;
    rows.forEach(function (r) { var hit = r.textContent.toLowerCase().indexOf(q) > -1; r.hidden = !hit; if (hit) shown++; });
    status.textContent = shown + (shown === 1 ? ' invoice' : ' invoices') + ' shown';
  });

  // Columns menu: each checkbox shows or hides one column (header and cells).
  menu.querySelectorAll('input[data-col]').forEach(function (box) {
    box.addEventListener('change', function () {
      var i = Number(box.getAttribute('data-col'));
      Array.prototype.forEach.call(table.rows, function (r) { if (r.cells[i]) r.cells[i].hidden = !box.checked; });
    });
  });
  // Escape or a click outside closes the menu.
  menu.addEventListener('keydown', function (e) { if (e.key === 'Escape' && menu.open) { menu.open = false; menu.querySelector('summary').focus(); } });
  document.addEventListener('click', function (e) { if (menu.open && !menu.contains(e.target)) menu.open = false; });

  // Density: a data-dense attribute on the table (its data-dense: utilities tighten the cells), with aria-pressed kept in sync so the button reports its state.
  var density = root.querySelector('[data-tb-density]');
  density.addEventListener('click', function () {
    var on = density.getAttribute('aria-pressed') !== 'true';
    density.setAttribute('aria-pressed', String(on));
    table.toggleAttribute('data-dense', on);
  });

  // Filter chips: remove one, or all; focus moves to the next control so it is never lost.
  function afterRemove(next) {
    if (!chips.querySelector('[data-tb-remove]')) { chips.hidden = true; search.focus(); }
    else (next || chips.querySelector('button')).focus();
  }
  chips.addEventListener('click', function (e) {
    var btn = e.target.closest('[data-tb-remove]');
    if (btn) {
      var chip = btn.closest('span'), next = chip.nextElementSibling && chip.nextElementSibling.querySelector('button') || chip.nextElementSibling;
      chip.remove(); status.textContent = 'Filter removed'; afterRemove(next);
    } else if (e.target.closest('[data-tb-clear]')) {
      chips.querySelectorAll('[data-tb-remove]').forEach(function (b) { b.closest('span').remove(); });
      status.textContent = 'All filters cleared'; afterRemove();
    }
  });
})();

About this set

The controls that sit above a data table, wired to an invoices table. A search field filters rows as you type and a hidden status line announces how many are shown. A Columns menu, a details element with a checkbox per column, shows and hides columns, including a Due date column that starts hidden; Escape or a click outside closes it. A Compact button with aria-pressed switches the table to tighter rows and shows its pressed state. Export is a plain button for your own handler. Under the bar, removable filter chips show the filters in effect; each has a named remove button, Clear all removes them together, and focus moves to the next chip or the search field so it is never lost. Use it as the header of any admin list. The table itself scrolls sideways in a focusable region on phones. The Bootstrap version uses .form-control, a .dropdown with auto-close set to outside so it stays open while you tick, and .table-sm for compact rows; the Tailwind version uses a details element, aria-pressed: and data-dense: utilities.

What’s included

  • Search that filters rows and announces the result count
  • Columns menu that shows and hides table columns
  • Density toggle with aria-pressed and compact rows
  • Removable filter chips with Clear all and managed focus
  • Bootstrap dropdown and .table-sm version

Accessibility

The search field and column checkboxes have labels, the density button reports its state with aria-pressed, and each chip's remove button names its filter. A visually hidden role="status" line announces search results and removed filters. Every control shows a 2px indigo focus outline, and the menu closes with Escape and returns focus to its button.

Customise it

The accent is #4f46e5 and the chips use #eef2ff and #3730a3 in the .tb rules; compact padding is the [data-dense] rule. In Bootstrap set the --bs-btn-*, --bs-dropdown-* and --bs-table-* variables and the .table-sm padding; in Tailwind edit the colour utilities and data-dense: padding.