Simple Filters in CSS or JS
Tags: javascript, css
Published: 2020-10-30 22:15:50

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>
```

###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>
```
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>
```

#####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;
}
```

#####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
  });
};
```

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

| | | | |
|:---:|:---:|:---:|:---:|
| ![Walk Filter](https://dev-to-uploads.s3.amazonaws.com/i/pu3tp5icoomwpsfc6bcu.png) | ![Swim Filter](https://dev-to-uploads.s3.amazonaws.com/i/xgvzsj9hepwv5r4evwtq.png) | ![Fly Filter](https://dev-to-uploads.s3.amazonaws.com/i/9jx2nbp9zm894l41zo5o.png) | ![All Filter - Error](https://dev-to-uploads.s3.amazonaws.com/i/jcr0csdwy6stq7cav8ra.png) |

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');
    });
  };
};
```
![Correct All Filter](https://dev-to-uploads.s3.amazonaws.com/i/bp7o2fl3lgy2rrkqsfv3.png)

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>
```

#####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;
}
```
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](https://www.w3.org/TR/selectors/#general-sibling-combinators).  

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;
}
```
#####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;
}
```

Now the moment of truth, does it work?

| | | | |
|:---:|:---:|:---:|:---:|
| ![Walk Filter](https://dev-to-uploads.s3.amazonaws.com/i/pu3tp5icoomwpsfc6bcu.png) | ![Swim Filter](https://dev-to-uploads.s3.amazonaws.com/i/xgvzsj9hepwv5r4evwtq.png) | ![Fly Filter](https://dev-to-uploads.s3.amazonaws.com/i/9jx2nbp9zm894l41zo5o.png) | ![All Filter - Error](https://dev-to-uploads.s3.amazonaws.com/i/jcr0csdwy6stq7cav8ra.png) |

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.