Blog (my web musings)

Find out what's interesting me, get tips, advice, and code gems that don't fit elsewhere.


Search Blog


The SlideUp / SlideDown methods in jQuery are said to be the "Holy Grail" of vertical animations, because they are able to (reasonably) smoothly change the height of a matched element with a sliding motion, while allowing sibling elements to move in to the empty space. If you're unsure of what I mean, take a look at this jQuery-powered Q&A / FAQs script I cobbled together, and watch how the pretty-coloured bars move to leave no gaps when other boxes are opened and closed.

jQuery is a power-house, with a great many attractive qualities and uses, but it is also a beast. It's a JavaScript library that has a low learning-curve, and cuts development time thanks to its well-tested, cross-browser compatible routines. The problem, however, is that it is huge. If you want your website to be as light-weight as possible, the extra ~100kb (minified) of jQuery bulk, is a difficult cross to bear. And if you're only including it on a web page to power one script, that just makes things all the more painful. Don't get me wrong - I love jQuery, along with all the fantastic plugins that cleverer people than me have written, but I hate the thought of having to rely on it.

So vanilla JavaScript, where possible, is now my thing; Using it to add and remove classes that, in turn, trigger CSS3 animations. But trying to recreate jQuery's SlideUp and SlideDown animations with CSS3 alone has proved to be very difficult indeed. In this blog post, author, James Steinbach, explains why CSS animation hasn't yet successfully solved the problem;

CSS Animations don't work on an unknown height

We can't transition between height:0; and height:auto; which puts a kink in animating those variable-height toggle boxes that jQuery handles so well.

Unpredictable easing and timing

You can transition between an explicit height value (but fixed heights don't play nicely with unpredictable user-added content or responsive design), or a max-height set as some large value, but this screws with easing and timing - something demonstrated perfectly here.

Transformed elements still exist in the stacking order

Content boxes that have been collapsed with a transform, still occupy space on the page so that sibling elements do not move in to fill the empty gaps - not in an animated fashion, not in any way at all.

CSS-Only Conclusion

So an animated CSS-only solution is still a no-go, which leaves us with the question "Do we really still need to use jQuery to power our sliding content / toggle boxes?". Well, to cut a long story short... No we don't.

Moving from jQuery to VelocityJS

If you haven't heard of it yet, VelocityJS is an animation engine for fast performance animations. It is as fast as CSS and delivers better performance than jQuery, particularly on mobile devices (you've probably seen janky jQuery drop-downs on mobile - Velocity is smoother). The syntax is also similar, allowing you to jump from one to the other with a minimal learning-curve. It's a third of the size of jQuery too (~33kb minified) and so doesn't have so much of a weight-impact on your web page loading time. The down-side is that you need to write your own vanilla JavaScript functions in order to trigger the animations, but that's what this blog post is about. Hopefully you will pick up the basics from this and the Silky Smooth Web Animation with VelocityJS article, and follow suit to a leaner, lighter website and codebase.

VelocityJS - SlideUp / SlideDown Q&A or FAQs:

Demo

JavaScript helper functions

The first thing I did after including the Velocity code in my page, was setup some helper functions. You can find various alternatives from You Might Not Need jQuery, plainJS, and Vanilla-Helpers - the ones I used are below;

/* Check if a class exists on an element */
function hasClass(el, cls){
    if (el.className.match('(?:^|\\s)'+cls+'(?!\\S)')) { return true; }
    }

/* Add a class if it doesn't exist on an element - replacement for jQuery .addClass() */
function addClass(el, cls){
    if (!el.className.match('(?:^|\\s)'+cls+'(?!\\S)')){ el.className += ' '+cls; }
    }

/* Delete a class if it exists on an element - replacement for jQuery .removeClass() */
function delClass(el, cls){
    el.className = el.className.replace(new RegExp('(?:^|\\s)'+cls+'(?!\\S)'),'');
    }

/* Find the element's previous sibling element - replacement for jQuery .prev() */
function prevElementSibling(el){
    if (el.previousElementSibling){ return el.previousElementSibling; }
    else { el = el.previousSibling; while (el.nodeType !== 1) { return el.previousSibling; } }
    }

Alternative SlideUp() and SlideDown() functions

Next, I created my alternative, Velocity-powered SlideUp() and SlideDown() functions - I called mine qaOpen() and qaClose() because I'm using them in my Q&A/FAQs script. The functions include a check for IE8 because VelocityJS animations will only work in IE8 if jQuery is present, and that just defeats the object of streamlining the code (in IE8, the boxes open but do not animate).

First up is the qaOpen() function. The line in red checks if a content box for my Q&A / FAQ is already open, and if it isn't, a Velocity slideDown animation is performed. At the same time, the ".item-open" class is applied;

function qaOpen(el){ 
    for(i=0; i<el.length; i++){
        if (!hasClass(prevElementSibling(el[i]), 'item-open')) {
           if (!isIE8andUnder) { Velocity(el[i], 'slideDown', { duration:500 }); }
           addClass(prevElementSibling(el[i]), 'item-open');
           }
        }
    }

As you'd expect, the qaClose() function is pretty much the same, but in reverse; A Velocity slideUp animation is performed, and the ".item-open" class is removed;

function qaClose(el){ 
    for(i=0; i<el.length; i++){
        if (!isIE8andUnder) { Velocity(el[i], 'slideUp', { duration: 500 }); }
        delClass(prevElementSibling(el[i]), 'item-open');
        }
    }

jQuery replacement code

Now on to the jQuery replacement code. I've put the jQuery functions and vanilla JS equivalents next to each other so that you can compare between the 2.

1a: Here's the jQuery toggle that opens and closes a Q&A / FAQ answer when the header-bar is clicked;

$('[data-goto-id]').click(function(){ // open qa-answer from same page 
    var id = $(this).data('goto-id');
    if($('#' + id).is(':hidden')){
        $('#' + id).slideDown(500).prev().addClass('item-open');
        } else {
        $('#' + id).slideUp(500).prev().removeClass('item-open');
        }
    });

1b: And here's the vanilla JS equivalent;

for(i=0; i<qaQuestion.length; i++){ // open qa-answer from same page 
    qaQuestion[i].onclick = function(){
       var el = document.querySelectorAll('#'+this.getAttribute('data-goto-id'));
       hasClass(this, 'item-open') ? qaClose(el) : qaOpen(el);
       }
    }

2a: Here's the jQuery to close all and open all Q&As / FAQs when the "Close All" or "Open All" buttons are clicked;

$('[data-goto-close="all"]').click(function(){ // close all 
   $('.qa-answer').slideUp(500).prev().removeClass('item-open');
   });

$('[data-goto-open="all"]').click(function(){ // open all
   $('.qa-answer').slideDown(500).prev().addClass('item-open');
   });

2b: And the replacement vanilla JS;

document.querySelector('[data-goto-open="all"]').onclick = function(){ qaOpen(qaAnswer) } // open all

document.querySelector('[data-goto-close="all"]').onclick = function(){ qaClose(qaAnswer) } // close all

3a: Here's the jQuery for opening all Q&As / FAQs in a group, and closing all Q&As / FAQs in a group, when the corresponding "Open Group" and "Close Group" buttons are clicked;

$('[data-goto-open]').click(function(){ // open all in group 
    $('[data-goto-group="' + $(this).data('goto-open') + '"]').slideDown(500).prev().addClass('item-open');
    });

$('[data-goto-close]').click(function(){ // close all in group
    $('[data-goto-group="' + $(this).data('goto-close') + '"]').slideUp(500).prev().removeClass('item-open');
    });

3b: And the replacement vanilla JS;

for(i=0; i<qaGroupOpen.length; i++){ // open all in group 
       qaGroupOpen[i].onclick = function(){
           var el = document.querySelectorAll('[data-goto-group="'+this.getAttribute('data-goto-open')+'"]');
           qaOpen(el);
           }
        }

for(i=0; i<qaGroupClose.length; i++){ // close all in group
       qaGroupClose[i].onclick = function(){
           var el = document.querySelectorAll('[data-goto-group="'+this.getAttribute('data-goto-close')+'"]');
           qaClose(el);
        }
}

4a: Lastly, the jQuery code for when actions are performed via a hashed URL from another page;

$(function(){ // open qa-answer from another page 
    var hash = window.location.hash.substr(1);
    $('#' + hash).slideDown(500).prev().addClass('item-open'); // open item by id
    $('[data-goto-group="' + hash + '"]').slideDown(500).prev().addClass('item-open'); // open group
    if(hash == 'all'){ $('.qa-answer').slideDown(500).prev().addClass('item-open'); } // open all
    });

4b: The hash action vanilla JS;

if (qaHash) { qaOpen(document.querySelectorAll('#'+qaHash)); } // open item by id
if (qaHash) { qaOpen(document.querySelectorAll('[data-goto-group="'+qaHash+'"]')); } // open group
if (qaHash == 'all') { qaOpen(qaAnswer); } // open all

Did you follow that? I hope so, and I also hope that you'll be a little bit braver in deciding if jQuery is really needed for your projects too.

Check out the demo for the complete Velocity-powered script.

Demo

Note that I was having a lazy moment when I embeded the VelocityJS code, rather than linking to a separate JS file. It looks big when you view the source, but don't be put off - it's much smaller than jQuery - you can extract it to an external file so that the workings of your web page look cleaner / smaller.