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.
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');
});
};
};
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.
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?
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.
Top comments (9)
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.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!
Nice trick.
Can we use event bubbling instead of writing the onClick for every div?[1st method]
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!
Yep 💥ðŸ¤
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
This would reduce JS to a minimum
with your kind help I ll got my concepts very clear
Thanks for this, it is really helpful! It might be a dumb question, but is there a way to make it work on mobile?