webdevpuneet.com

Filterable Table — Per-Column Filter HTML CSS JS

Filterable Table · Tables · Plain HTML, CSS & JS · Live preview

CategoryTables

Copy & paste

Source Code

<div class="flt-wrap">
  <div class="flt-bar">
    <h3>Employees</h3>
    <span class="flt-count" id="fltCount"></span>
    <button type="button" class="flt-clear" id="fltClear">Clear filters</button>
  </div>
  <table class="flt-table">
    <thead>
      <tr>
        <th>Name<input class="flt-f" data-col="0" placeholder="Filter…"></th>
        <th>Department<input class="flt-f" data-col="1" placeholder="Filter…"></th>
        <th>Location<input class="flt-f" data-col="2" placeholder="Filter…"></th>
        <th class="flt-num">Salary<input class="flt-f" data-col="3" placeholder="≥ min"></th>
      </tr>
    </thead>
    <tbody id="fltBody"></tbody>
  </table>
  <p class="flt-empty" id="fltEmpty" hidden>No rows match your filters.</p>
</div>

Extend with AI

Take This Snippet Further With AI

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

· Become a supporter

What's included

Features

Per-column filter inputs
A filter input in every header, each bound to its column index.
AND-combined matching
Rows must satisfy every active filter, so criteria stack to narrow results.
Text and numeric modes
Text columns use substring matching; the numeric column treats input as a minimum threshold.
Safe match highlighting
Matched substrings are wrapped in <mark> after HTML-escaping, so data can't inject markup.
Live result count
A "N of M" counter shows how much the filters have narrowed the table.
Empty state
A friendly message appears when no rows match instead of a blank table.
One-click clear
A Clear filters button resets every column input and re-renders.
Data-driven & no library
Renders from a ROWS array with generic column logic — zero dependencies.

About this UI Snippet

Filterable Table — Per-Column Header Filters with Live AND Matching & Highlighting

Screenshot of the Filterable Table snippet rendered live

A single global search box is fine for "find anything," but when users need to narrow a table by specific fields — department *and* location *and* salary — per-column filters are far more powerful. This snippet builds a table with a filter input in every column header, combining them with AND logic so each keystroke narrows the result, plus match highlighting, a numeric minimum filter, and a live count — all in plain HTML, CSS, and vanilla JavaScript with no library.

A filter per column, combined with AND

Each header carries its own input bound to a column index. As the user types in any of them, the matching value is stored in a filters array and the table re-renders, showing only rows that satisfy *every* active filter. This AND combination is the whole point: typing "Engineering" in Department and "Berlin" in Location shows only people who are both, which a single search box can't express. Empty filters are ignored, so the table starts unfiltered and narrows as criteria are added.

Text columns vs. the numeric column

Text columns use case-insensitive substring matching — the forgiving behaviour users expect from a filter. The salary column is treated differently: its filter is parsed as a *minimum*, so typing "100000" shows everyone earning at least that. Recognising that numbers want a threshold filter, not a substring match (you don't search a salary for the characters "100000"), is the detail that makes per-column filtering genuinely useful rather than naïve.

Match highlighting

When a text filter is active, the matching substring in each cell is wrapped in a <mark>, so the user sees exactly why a row matched. The highlighting is built safely: all cell text is HTML-escaped first, then only the matched slice is wrapped, so data containing < or & can never inject markup. This escape-then-wrap order is the safe way to do highlight rendering with innerHTML.

Live count, empty state, and clear

A counter shows "N of M" so the user always knows how much the filters have narrowed the set, and a friendly empty state appears when nothing matches rather than a blank table. A "Clear filters" button resets every input and filter in one click. These three touches — count, empty state, reset — are what separate a finished filterable table from a bare filter() demo.

Data-driven and drop-in

Rows come from a ROWS array, and the filtering logic is generic over columns, so adding a column is a markup-plus-data change, not a rewrite. Because it's dependency-free, it drops into any admin panel or report, and it's a clear reference for per-column AND filtering, safe highlight rendering, and mixed text/numeric matching.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSAn employees table renders with a filter input in every column header and a row count.
  2. 2
    Filter by columnType in any header input — the table narrows live, combining all active filters with AND logic.
  3. 3
    Use the numeric filterType a number in the Salary filter to show only rows at or above that minimum.
  4. 4
    See highlights and countMatching text is highlighted, and the "N of M" count shows how much you've narrowed the set.
  5. 5
    Clear filtersClick Clear filters to reset every column input at once.
  6. 6
    Swap in your dataReplace the ROWS array (and headers) with your own columns; the filtering logic is generic.

Real-world uses

Common Use Cases

Admin and HR directories
Let staff narrow large people tables by department and location — pair with a data table for sorting and actions.
Product and inventory lists
Filter catalogs by multiple fields at once alongside a sortable table.
Reports and dashboards
Slice operational data per column next to a bar chart of the totals.
Log and event browsers
Narrow logs by level, source, and message with AND filters.
Order and ticket queues
Filter by status, owner, and amount, complementing a bulk actions bar.
Learning table filtering
A reference for per-column AND filtering and safe highlighting — compare with an expandable table.
Related: Shift-Click Range Select in a Table (Gmail/Sheets-Style)
See the Shift-Click Range Select in a Table (Gmail/Sheets-Style) for a related tables pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

Each header input writes its value into a filters array keyed by column index, and a row is shown only if it satisfies every active filter (AND logic). Empty filters are skipped, so the table starts unfiltered and narrows as you add criteria. This lets users express "Engineering AND Berlin AND salary ≥ 100k" — something a single global search box cannot do.

Numbers want a threshold, not a substring match — you don't search a salary for the literal characters "100000". The salary filter is parsed to a number and used as a minimum, so typing 100000 shows everyone earning at least that. Text columns keep case-insensitive substring matching, which is the forgiving behaviour expected for names and categories.

Yes. Every cell's text is HTML-escaped first, and only then is the matched slice wrapped in <mark>. Because the escaping happens before any markup is added, data containing <, >, or & is rendered as text and can never inject elements — the correct escape-then-wrap order for highlight rendering with innerHTML.

Add a <th> with a filter input (set its data-col to the new index) and include the value in each ROWS entry. The rowMatches and render functions iterate columns generically, so text columns work automatically; for another numeric/threshold column, extend the numeric branch in rowMatches to that index.

In React, hold the rows and a filters object in useState, derive the visible rows with a useMemo over the filters, and render highlighted cells; in Vue, use a computed filtered list with v-for; in Angular, use a pipe or a getter with *ngFor. The matching and highlight logic is framework-agnostic — only the state and re-render move into the framework.