Client-side table filtering with CSS selectors

Here's a cool trick I came up with (pretty sure it's been done already, but I've never heard of this particular one) that makes it really easy to add list or table filtering/searching in a "boring" web application (i.e. one that doesn't use a SPA library or framework) in around 70 lines of (reusable) Javascript and 10 lines of CSS.

As an example, let's say we have a list of entries. Each entry has a title and one or more authors. In my particular case, we fetch the entries from a JSON endpoint and render each one using Handlebars, but it could be easily server-side rendered. The entries are not enough to warrant pagination or server-side searching, but enough to make it difficult to find a single entry you're looking for.

We turn out to client side filtering: render all the data in the DOM, but allow the user to narrow it down as they type. In our case, to keep things explicit, we want two separate search boxes, one that filters only by title, and one that filters by author. We also only care about filtering while maintaining the server-side ordering (so no client side sorting).

Here is what a pseudo-handlebars template could look like for each row:

<tr class="entry-row">  
  <td>{{name}}</td>
  <td>{{#each authors}}{{this.name}}{{#unless @last}}, {{/unless}}{{/each}}</td>
</tr>  

What we are going to do is add a couple of attributes to the row:

<tr class="entry-row" data-filterable data-name="{{lowercase name}}" data-authors="{{#each authors}}{{lowercase this.name}} {{/each}}">  
  <td>{{name}}</td>
  <td>{{#each authors}}{{this.name}}{{#unless @last}}, {{/unless}}{{/each}}</td>
</tr>  

We are adding:

  • data-filterable - a generic property that all the entries in this table will share
  • data-name, data-authors - the information that will control the search

Up until now this is a very common approach, decorate-then-search. The cool part comes next:

Let's say the user has typed "terry" in the author search box. It turns out we can write two simple CSS selectors:

const matchSelector = '[data-filterable][data-authors*="terry"]';  
const reverseSelector = '[data-filterable]:not([data-authors*="terry"])'  

First, we narrow down the selectors to only elements that have a data-filterable attribute. Then, we search for elements that have "terry" anywhere in the value of the data-authors attribute. For the reverse we use the :not() pseudo-selector to just reverse it - it's almost symmetrical to the first one so really trivial to create.

We can then do:

const matchingElements = document.querySelectorAll(matchSelector);  
const notMatchingElements = document.querySelectorAll(reverseSelector);  

...to get two mutually exclusive NodeLists. You can then iterate over them and add/remove classes that show/hide them as wanted. To clear the search, just combine them and remove all the search-related classes.

What about searching entries that have both "terry" in their authors and "omens" in their title?

const matchSelector = '[data-filterable][data-authors*="terry"][data-title*="omens"]';  

You can go on adding selectors as needed, perhaps adding a simple query language to search entries that have "terry" AND "neil" in their authors. The resulting selector would look like this:

const matchSelector = '[data-filterable][data-authors*="terry"][data-authors*="neil"]';  

The nice thing about all this is that all the logic is moved to the CSS selectors, and all the search function cares about is really two mutually-exclusive lists of elements.

There are obvious limitations but for my use case it was extremely useful and performance seems lightning fast (as the actual searching is done by the browser and by Javascript traversing the DOM to check every element).