loading...
Cover image for Simple Filters in CSS or JS

Simple Filters in CSS or JS

dhintz89 profile image Daniel Hintz ・7 min read

In this post, I want to demonstrate how to add a simple and flexible filtering solution to a website. The use case here is that I have a collection of artifacts - in my case portfolio projects, but here we'll simplify to animals - and I want to be able to:

  • Filter by clicking a button (or div, etc.)
  • Easily add new items to the collection without changing any code.

I will explore two different methods of applying the same filters to the same data, one based in JavaScript, the other based in CSS alone.

Let's start by creating the html for the filters and the collection of animals, we'll represent the filters as buttons and create a div for each animal:

<div class="filters">
  <h3>Filters</h3>
  <button class="filter-option">Walks</button>
  <button class="filter-option">Swims</button>
  <button class="filter-option">Flies</button>
  <button class="filter-option">All</button>
</div>

<div class="list">
  <h3>Animals</h3>
  <div class="dog">Dog</div>
  <div class="eagle">Eagle</div>
  <div class="cow">Cow</div>
  <div class="shark">Shark</div>
  <div class="canary">Canary</div>
  <div class="human">Human</div>
  <div class="salamander">Salamander</div>
</div>
Enter fullscreen mode Exit fullscreen mode

JS Filters - the more traditional way

There are, of course a lot of ways to filter using JavaScript. For this, I want to make sure that it's flexible enough to cover anything I add in later because I don't want to have to come back to edit the JS function. To do this I know that I'll need a way to identify which animals to include/exclude for each filter, and I'll want the HTML to do most of the heavy-lifting so I can add to the collection solely by adding HTML.

HTML

To start, I'll add a class to each animal div with the name of the relevant filter(s). This will be my identifier.

<div class="list">
  <h3>Animals</h3>
  <div class="dog walks">Dog</div>
  <div class="eagle flies">Eagle</div>
  <div class="cow walks">Cow</div>
  <div class="shark swims">Shark</div>
  <div class="canary flies">Canary</div>
  <div class="human walks">Human</div>
  <div class="salamander swims walks">Salamander</div>
</div>
Enter fullscreen mode Exit fullscreen mode

Notice that the last item, Salamander, can walk or swim. We'll need to make sure that our filter function can handle items belonging to multiple criteria.

Next, I also know that I'll need to add an event listener to each of the filters to call my JS function. Somehow, I want to pass the filter value to the function as well. We could write the event listener call like onclick="filterAnimals('walks')" but it might be nice to be able to grab the value of the filters in other code too, so let's instead put the value as an HTML data- attribute and use that in our function instead. That has an added side effect of making the code a little more readable as well.

<div class="filters">
  <h3>Filters</h3>
  <button class="filter-option" data-filter="walks" onclick=filterAnimals(event)>Walks</button>
  <button class="filter-option" data-filter="swims" onclick=filterAnimals(event)>Swims</button>
  <button class="filter-option" data-filter="flies" onclick=filterAnimals(event)>Flies</button>
  <button class="filter-option" data-filter="*" onclick=filterAnimals(event)>All</button>
</div>
Enter fullscreen mode Exit fullscreen mode
CSS

Now it's time to determine how to actually get the items to filter. In CSS, we can essentially remove an element from the page by setting it to display: none. Let's create a class that has that setting, so our JS code can simply add/remove that class as needed...Well that was easy.

.hidden {
  display: none;
}
Enter fullscreen mode Exit fullscreen mode
JavaScript

What's left to do? When we select a filter, our JavaScript only needs to go through the animals to see if they contain the filter as a class. If they do, they should not get the .hidden class, if they do not, then they do get that class added.

function filterAnimals(e) {
  const animals = document.querySelectorAll(".list div"); // select all animal divs
  let filter = e.target.dataset.filter; // grab the value in the event target's data-filter attribute
  animals.forEach(animal => {
    animal.classList.contains(filter) // does the animal have the filter in its class list?
    ? animal.classList.remove('hidden') // if yes, make sure .hidden is not applied
    : animal.classList.add('hidden'); // if no, apply .hidden
  });
};
Enter fullscreen mode Exit fullscreen mode

Great! Now our filters should work, let's take a look.

Walk Filter Swim Filter Fly Filter All Filter - Error

Notice our troublemaker Salamander does show up in both the walks and the swims filter, that's great news! However, look at the all filter...not so good. We know there is data so surely there should be something there right? Unfortunately, we don't have an .all class in any of our artifacts, so that filter won't match anything. We could add .all to every animal, but it would be much cleaner and easier for our JavaScript to handle that. We just need an if/else statement to determine whether the filter is "all" or something more specific:

// code to add:
if (filter === '*') {
  animals.forEach(animal => animal.classList.remove('hidden'));
}

// full JS code:
function filterAnimals(e) {
  const animals = document.querySelectorAll(".list div");
  let filter = e.target.dataset.filter;
  if (filter === '*') {
    animals.forEach(animal => animal.classList.remove('hidden'));
  }  else {
    animals.forEach(animal => {
      animal.classList.contains(filter) ? 
      animal.classList.remove('hidden') : 
      animal.classList.add('hidden');
    });
  };
};
Enter fullscreen mode Exit fullscreen mode

Correct All Filter

There we go, now we're all set. If we want to add something later, like an ostrich, we just need to put in a line of HTML:
<div class="ostrich walks">Ostrich</div>
Everything else is taken care of for us like magic.

CSS Filter

Let's see how to implement the same thing, but without using any JavaScript at all. It involves a neat CSS trick!

HTML

First thing, we don't need any event listeners anymore since there's no JS functions to call, so let's get rid of those. Otherwise everything is the same.

<div class="filters">
  <h3>Filters</h3>
  <button class="filter-option" data-filter="walks">Walks</button> 
  <button class="filter-option" data-filter="swims">Swims</button> 
  <button class="filter-option" data-filter="flies">Flies</button> 
  <button class="filter-option" data-filter="*">All</button>
</div>

<div class="list">
  <h3>Animals</h3>
  <div class="dog walks">Dog</div>
  <div class="eagle flies">Eagle</div>
  <div class="cow walks">Cow</div>
  <div class="shark swims">Shark</div>
  <div class="canary flies">Canary</div>
  <div class="human walks">Human</div>
  <div class="salamander swims walks">Salamander</div>
</div>
Enter fullscreen mode Exit fullscreen mode
CSS

But how can CSS actively filter for us? The key to that question is the "actively" part. When a user clicks a button, it is in focus until the user clicks elsewhere. So, we can use that to our advantage by adding a :focus selector to each button. We can also access our data- attributes using CSS to determine which filter to apply when a given button is in focus.
button[data-filter="walks"]:focus

We also know we need the animals that are filtered out to receive the display: none attribute.

button[data-filter="walks"]:focus {
  display:none;
}
Enter fullscreen mode Exit fullscreen mode

But the challenge is how to actually select the animals, rather than the button, when we have the button in focus? We can use ~ to select "elements that follow an element at the same level." This is officially called the "general-sibling-combinator" More Info Here.

The only issue is that this requires the animals and the filters to share a parent element, which they currently do not, so we'll need to make a minor update to our HTML to make that happen by combining everything under a single div, let's give it a .filteredList class.

With that change made, we can now use ~ to select "divs sharing the same parent as the selected button, whose class contains the data-filter attribute value from the button." Here's how that looks (*= means 'contains' where = would require an exact match):

button[data-filter="walks"]:focus ~ div:not([class*="walks"]) {
  display:none;
}

button[data-filter="swims"]:focus ~ div:not([class*="swims"]) {
  display:none;
}

button[data-filter="flies"]:focus ~ div:not([class*="flies"]) {
  display:none;
}
Enter fullscreen mode Exit fullscreen mode
JavaScript

There is no JavaScript - woohoo!

Full Code
// HTML
  <div class="filteredList">
    <h3>Filters</h3>
    <button class="filter-option" data-filter="walks" tabindex="-1">Walks</button> 
    <button class="filter-option" data-filter="swims" tabindex="-1">Swims</button> 
    <button class="filter-option" data-filter="flies" tabindex="-1">Flies</button> 
    <button class="filter-option" data-filter="*" tabindex="-1">All</button>

    <h3>Animals</h3>
    <div class="dog walks">Dog</div>
    <div class="eagle flies">Eagle</div>
    <div class="cow walks">Cow</div>
    <div class="shark swims">Shark</div>
    <div class="canary flies">Canary</div>
    <div class="human walks">Human</div>
    <div class="salamander swims walks">Salamander</div>
  </div>

//CSS
button[data-filter="walks"]:focus ~ div:not([class*="walks"]) {
  display:none;
}

button[data-filter="swims"]:focus ~ div:not([class*="swims"]) {
  display:none;
}

button[data-filter="flies"]:focus ~ div:not([class*="flies"]) {
  display:none;
}
Enter fullscreen mode Exit fullscreen mode

Now the moment of truth, does it work?

Walk Filter Swim Filter Fly Filter All Filter - Error

It Works!! Keep in mind that if you click anywhere else on the page, the filters will be removed (because the button is out of focus). And finally, how would we add our new ostrich animal? Exactly the same way:
<div class="ostrich walks">Ostrich</div>

Overall, the JavaScript function is probably going to be the better way to go in nearly all situations, but I thought this was a cool CSS trick and it could be useful if you want a lightweight quick-filter feature.

Let me know what you think in the comments.

Discussion

pic
Editor guide
Collapse
sirpilan profile image
sirpilan

Nice one, there actually is a trick to get a persistent state without JS: Have a look here:
stackoverflow.com/questions/630047...
"Without JS - height transition".

Basically you use a input checkbox - then use input:checked in css to filter. By doing this, you will be able to filter persistently and combine filters aswell.

Collapse
dhintz89 profile image
Daniel Hintz Author

Ooohh clever, I like it! I'll try to implement that approach in this example in the next week or two and add it in (and give you a shoutout, of course 😉). Thanks for sharing!

Collapse
asifurrahamanofficial profile image
Asifur Rahaman

Nice trick.
Can we use event bubbling instead of writing the onClick for every div?[1st method]

Collapse
dhintz89 profile image
Daniel Hintz Author

Hi Asifur, thanks for the suggestion. While I understand the basics of event bubbling, I have never really looked into how to beneficially implement it. I saw your article about it, so I'm going to try out the delegation approach here shortly. Sounds like a super useful trick!

Collapse
asifurrahamanofficial profile image
Asifur Rahaman

Yep 💥🤠

Collapse
dakri profile image
Dakri

You could also combine JS and CSS to create your own focus state, that will not get resetted by clicking anywhere, so CSS still does the hiding-logic instead of JS loops

Collapse
dakri profile image
Dakri

This would reduce JS to a minimum