How to Filter a Table with JavaScript
This recipe applies the same live-filtering technique to a table, checking each row's relevant cell instead of an entire list item.
Adapting list-filtering to table rows
A table adds one wrinkle that a plain list doesn't have: structure. A <li> is just a blob of text, but a table row is made of separate cells with different meanings — a name, an email, a price. Filtering "the whole row's text" would technically work, but it usually isn't what you want, because a search for a name shouldn't also match unrelated numbers in a price column. The more useful pattern is to filter each row by a specific cell, chosen by its column position, and hide or show the entire <tr> based on that one cell's match.
- Select the rows to filter with document.querySelectorAll('tbody tr') — scoping to tbody keeps the header row out of the loop
- For each row, grab the target cell with row.cells[index], where index is the zero-based column position
- Lowercase the cell's textContent and compare it to the lowercased query with .includes(), exactly as with a list
- Toggle display on the <tr> itself, not on the individual <td> — hiding one cell would misalign the rest of the row
Example
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Filter a Table</title>
<style>
body {
font-family: system-ui, sans-serif;
padding: 40px;
background: #f5f5f7;
}
input[type="search"] {
width: 260px;
padding: 8px 12px;
font-size: 15px;
border: 1px solid #ccc;
border-radius: 6px;
margin-bottom: 12px;
box-sizing: border-box;
}
table {
border-collapse: collapse;
width: 100%;
max-width: 480px;
background: #fff;
}
th, td {
text-align: left;
padding: 8px 12px;
border-bottom: 1px solid #eee;
}
th {
background: #f0f0f0;
}
</style>
</head>
<body>
<input type="search" id="nameFilter" placeholder="Search by name…" aria-label="Search by name">
<table>
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Role</th>
</tr>
</thead>
<tbody>
<tr><td>Amara Diallo</td><td>amara@example.com</td><td>Designer</td></tr>
<tr><td>Ben Cohen</td><td>ben@example.com</td><td>Developer</td></tr>
<tr><td>Chen Wei</td><td>chen@example.com</td><td>Manager</td></tr>
<tr><td>Diego Ramos</td><td>diego@example.com</td><td>Developer</td></tr>
<tr><td>Emma Novak</td><td>emma@example.com</td><td>Designer</td></tr>
</tbody>
</table>
<script>
const input = document.getElementById('nameFilter');
const rows = document.querySelectorAll('table tbody tr');
input.addEventListener('input', () => {
const query = input.value.trim().toLowerCase();
rows.forEach((row) => {
const nameCell = row.cells[0];
const matches = nameCell.textContent.toLowerCase().includes(query);
row.style.display = matches ? '' : 'none';
});
});
</script>
</body>
</html>When restoring a hidden row, set row.style.display = '' rather than hardcoding row.style.display = 'table-row'. Clearing the inline style lets the browser fall back to whatever the element's default or stylesheet-defined display value actually is — for a <tr> that's normally table-row, but if a stylesheet elsewhere overrides it, hardcoding the value fights that override instead of cooperating with it.
Example
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Filter a Table by Chosen Column</title>
<style>
body {
font-family: system-ui, sans-serif;
padding: 40px;
background: #f5f5f7;
}
.controls {
display: flex;
gap: 8px;
margin-bottom: 12px;
}
input[type="search"], select {
padding: 8px 12px;
font-size: 15px;
border: 1px solid #ccc;
border-radius: 6px;
box-sizing: border-box;
}
table {
border-collapse: collapse;
width: 100%;
max-width: 480px;
background: #fff;
}
th, td {
text-align: left;
padding: 8px 12px;
border-bottom: 1px solid #eee;
}
th {
background: #f0f0f0;
}
</style>
</head>
<body>
<div class="controls">
<select id="columnSelect" aria-label="Column to search">
<option value="0">Name</option>
<option value="1">Email</option>
</select>
<input type="search" id="tableFilter" placeholder="Type to filter…" aria-label="Filter value">
</div>
<table>
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Role</th>
</tr>
</thead>
<tbody>
<tr><td>Amara Diallo</td><td>amara@example.com</td><td>Designer</td></tr>
<tr><td>Ben Cohen</td><td>ben@example.com</td><td>Developer</td></tr>
<tr><td>Chen Wei</td><td>chen@example.com</td><td>Manager</td></tr>
<tr><td>Diego Ramos</td><td>diego@example.com</td><td>Developer</td></tr>
<tr><td>Emma Novak</td><td>emma@example.com</td><td>Designer</td></tr>
</tbody>
</table>
<script>
const columnSelect = document.getElementById('columnSelect');
const input = document.getElementById('tableFilter');
const rows = document.querySelectorAll('table tbody tr');
function applyFilter() {
const columnIndex = Number(columnSelect.value);
const query = input.value.trim().toLowerCase();
rows.forEach((row) => {
const cell = row.cells[columnIndex];
const matches = cell.textContent.toLowerCase().includes(query);
row.style.display = matches ? '' : 'none';
});
}
input.addEventListener('input', applyFilter);
columnSelect.addEventListener('change', applyFilter);
</script>
</body>
</html>Scaling up and avoiding gotchas
- Always scope your row selector to tbody — filtering table tr directly will try to hide the header row along with the data rows
- For large tables (roughly 1000+ rows), consider caching each row's lowercased text once instead of recomputing it on every keystroke, or debounce the input handler
- Show an empty-state row or message when zero rows match, so the table doesn't just look broken or empty with no explanation
Once a table can be filtered by one column, extending it to search multiple columns, add per-column filters, or combine filtering with sorting is mostly a matter of repeating this same match-then-toggle logic per row — the underlying technique doesn't change.
Exercise: Filters
What's the typical way to filter a list of already-loaded items by category on the client side?
Frequently Asked Questions
- How do I filter table rows with JavaScript?
- Loop the
<tbody>rows on each input event and setrow.style.displaytononewhen no cell matches the query. Skip the header row, or it disappears the moment someone types. - How do I search all columns of a table at once?
- Join each row's cell text into a single string and test the query against that, rather than checking one column.
row.textContentgives you the whole row's text in one step and matches across every column.