Building a Filtering Component with CSS Animations & jQuery

By George Martsoukos

Some months ago, I wrote an article about MixItUp, a popular jQuery plugin for filtering and sorting. In today’s article, I’ll show you how to build your own simple filterable component with jQuery and CSS animations.

Without further ado, let’s get started!

Setting Up the HTML

As a first step, I’ll show you the HTML structure of the component. Consider the following markup:

<div class="cta filter">
  <a class="all active" data-filter="all" href="#" role="button">Show All</a>
  <a class="green" data-filter="green" href="#" role="button">Show Green Boxes</a>
  <a class="blue" data-filter="blue" href="#" role="button">Show Blue Boxes</a>
  <a class="red" data-filter="red" href="#" role="button">Show Red Boxes</a>

<div class="boxes">
  <a class="red" data-category="red" href="#">Box1</a>
  <a class="green" data-category="green" href="#">Box2</a>
  <a class="blue" data-category="blue" href="#">Box3</a>

 <!-- other anchor/boxes here ... -->


Notice that I’ve set up some pretty basic markup. Here’s an explanation of it:

  • First, I’ve defined the filter buttons and the elements that I want to filter (we’ll call them target elements).
  • Next, I’ve grouped the target elements into three categories (blue, green, and red) and I gave them the data-category attribute. The value of this attribute determines the category that each element belongs to.
  • I’ve also assigned the data-filter attribute to the filter buttons. The value of this attribute specifies the desired filter category. For instance, the button with the data-filter="red" attribute/value will only show the elements that belong to the red category. On the other hand, the button with data-filter="all" will show all the elements.

Now that you’ve had an overview of the required HTML, we can move on to explore the CSS.

Setting Up the CSS

Each time a filter category is active, its corresponding filter button receives the active class. By default, the button with the data-filter="all" attribute gets this class.

Box with active class

Here are the associated styles:

.filter a {
  position: relative;

.filter {
  content: '';
  position: absolute;
  left: 0;
  top: 0;
  display: inline-block;
  width: 0;
  height: 0;
  border-style: solid;
  border-width: 15px 15px 0 0;
  border-color: #333 transparent transparent transparent;

In addition, I’m going to use flexbox to create the layout for the target elements.

Using flexbox for the layout

See the related styles below:

.boxes {
  display: flex;
  flex-wrap: wrap;

.boxes a {
  width: 23%;
  border: 2px solid #333;
  margin: 0 1% 20px 1%;
  line-height: 60px;

Lastly, I’m defining two different CSS keyframe animations that I’ll use later on to reveal the elements:

@keyframes zoom-in {
  0% {
   transform: scale(.1);
  100% {
    transform: none;

@keyframes rotate-right {
  0% {
    transform: translate(-100%) rotate(-100deg);
  100% {
    transform: none;

.is-animated {
  animation: .6s zoom-in;
  // animation: .6s rotate-right; 

With the markup and CSS in place, we can start building the JavaScript/jQuery.

Setting Up the jQuery

Have a look at the code below, after which I’ll explain what’s happening:

var $filters = $('.filter [data-filter]'),
    $boxes = $('.boxes [data-category]');

$filters.on('click', function(e) {
  var $this = $(this);

  var $filterColor = $this.attr('data-filter');

  if ($filterColor == 'all') {
      .fadeOut().promise().done(function() {
  } else {
      .fadeOut().promise().done(function() {
        $boxes.filter('[data-category = "' + $filterColor + '"]')

Each time a filter button is clicked, the following happens:

  • The active class is removed from all buttons and assigned only to the selected button.
  • The value of the button’s data-filter attribute is retrieved and evaluated.
  • If the value of data-filter is all, all elements should appear. To do so, I first hide them and then, when all elements become hidden, I show them using the rotate-right or zoom-in CSS animations.
  • If the value is not all, the target elements of the corresponding category should appear. To do so, I first hide all elements and then, when all of them become hidden, I show only the elements of the associated category using the rotate-right or zoom-in CSS animations.

At this point, it’s important to clarify one thing. Notice the syntax for the fadeOut() method in the above code. It looks as follows:

$boxes.fadeOut().promise().done(function() {
  // callback's body

You’re probably more familiar with this syntax though:

$boxes.fadeOut(function() {
  // callback's body

These declarations have different meanings:

  • In the first case, the callback is executed only after all target elements become hidden. You can learn more info about this approach by visiting the promise() section of jQuery’s docs.
  • In the second case, the callback is fired multiple times. Specifically, it’s executed each time an element becomes hidden.

The demo below uses the zoom-in animation:

See the Pen A sorting/filtering component with CSS and jQuery (with zoom animation) by SitePoint (@SitePoint) on CodePen.

And this demo uses the rotate-right animation:

See the Pen A sorting/filtering component with CSS and jQuery (with rotate animation) by SitePoint (@SitePoint) on CodePen.

Of course, the aforementioned CSS animations are optional. If you don’t like these specific animations, feel free to remove them and reveal the elements using only jQuery’s fadeIn() method.

Now that you understand how the component works, I’ll show you how to create a different variation of it.

Animating Elements Sequentially

Until now, you may have noticed that all elements appear at the same time. I’ll now modify the code to show them sequentially:

$filters.on('click', function(e) {

  // same code as above here

  if ($filterColor == 'all') {
      .fadeOut().finish().promise().done(function() {
        $boxes.each(function(i) {
          $(this).addClass('is-animated').delay((i++) * 200).fadeIn();
  } else {
      .fadeOut().finish().promise().done(function() {
        $boxes.filter('[data-category = "' + $filterColor + '"]').each(function(i) {
          $(this).addClass('is-animated').delay((i++) * 200).fadeIn();

The code above looks similar to the previous one but there are a few distinct differences:

  • First, I use the each() method to iterate through the target elements. Plus, as it loops, I’m getting the index of the current element (which is zero-based) and multiplying it by a number (e.g. 200). The derived number is passed as an argument to the delay method. This number indicates the amount of milliseconds that each element should wait before fading in.
  • Next, I use the finish() method to stop the currently-running animations for the selected elements under specific cases. To understand its usage, here’s a scenario: Click on a filter button and then, before all elements appear, click on the button again. You’ll notice that all elements disappear. Similarly, run this test again after removing the two instances of this method. In such a case, you’ll see that the elements receive some undesired effects. Sometimes calling this method properly can be tricky. For this example, I had to experiment a bit until I found where I should place it.

The demo below animates the filtered elements sequentially using the zoom-in animation:

See the Pen Sequential filtering/sorting component with CSS & jQuery by SitePoint (@SitePoint) on CodePen.

The demo below animates the filtered elements sequentially using the rotate-right animation:

See the Pen Sequential filtering/sorting with CSS and jQuery by SitePoint (@SitePoint) on CodePen.


This same component could be built without jQuery and may have better performance, but the ability to use jQuery’s fadeIn() and fadeOut() methods allows for simpler code that takes advantage of certain features available to jQuery.

Let me know in the comments if you have a different solution, or a way to improve the code.

  • Sue Bless

    With everybody’s working itineraries, sometimes our acne treatment regimens are able to slip by the wayside.

  • Vincent

    The controls should be buttons. If that is not possible add role=”button” to the links.

    • LouisLazaris

      Yes, you’re absolutely right. I’ve added the roles, but to be honest, I would rather use button elements, rather than reshaping anchors. For the purposes of these demos, I don’t think it matters much, but for those copying the code: Use buttons, not anchors.

      • Vincent

        Agreed, button elements are best. It matters. Look what happened to Angular accessibility when Google left it out in demos :(

  • Greg Vissing

    Would it be possible to only show 4 to start then have a Load More button to reveal 4 more at a time?

    • Yeah, that’s possible but you have to change the logic. If I come up with an easy solution, I’ll post it here.

      • Greg Vissing

        Hello! I ended up adding a “hide” class to all the div’s to hide them then had jQuery remove the class “hide” from the first four div’s. Then each time “Load More” is clicked that same function fires showing the next for until there are no more shows to display and a message appears at the end saying “No More Shows” and the “Load More” button fades out.

  • These are just a few reset rules that I set up for the demo layout. Your layout will probably be different, so it doesn’t make any sense to copy the reset rules. But if you want to do so, you can limit the scope. For example instead of the reset rule: a {...}, use this one: .filter a, .boxes a {...}

  • Michael McGuire

    Nice! Personally, I would use the jQuery to replace class names and let the css handle the fade action, but who’s using fadeIN/fadeOut works great. The next logical step would be to make it work with AJAX content loaded into memory and show/hide that as needed.

  • sabrina moldenhauer

    Hi! I just have a short question. I used the “A filtering component with CSS and jQuery

    (with zoom animation)” version. It works with Chrome and Safari but if I use Firefox or Edge to view the site there are gaps like a picture is missing. Has anyone an idea? What could be the problem?

    • George Martsoukos

      Hi, unfortunately I cannot understand what’s your problem. Where are gaps? Can you please create a screenshot or demo indicating the issue?

Get the latest in Front-end, once a week, for free.