In a mobile app I’ve been working on there are a few animations to drop down additional content as needed when clicking on an item. The effect is basically that I touch or click on an item and that then a menu drops down below. Another click or touch and the menu slides back up.
Here’s what it looks like (from geocrumbs.net):
In the past I’ve used jQuery’s .slideDown() and .slideUp() methods to do this sort of thing, but if you actually try running that on most phones today, you’ll find that the animation is pretty jerky as it runs without hardware rendering assistance using only the CPU rather than the GPU.
CSS Transitions 101
CSS transitions often make it easy to replace effects with CSS animations that render much smoother on slower devices and computers assuming you’re running a browser that’s reasonably recent. Most CSS transitions are super easy to create and use.
For example here’s a simple fade in and fade out animation tied to a a couple of CSS classes (example on Plunker):
.fadein, .fadeout {
opacity: 0;
-moz-transition: opacity 0.4s ease-in-out;
-o-transition: opacity 0.4s ease-in-out;
-webkit-transition: opacity 0.4s ease-in-out;
transition: opacity 0.4s ease-in-out;
}
.fadein {
opacity: 1;
}
Transitions work by processing a change of a value of a CSS property. Any property that has numeric values works can be transitioned in this way. Some examples are opacity, height and width, color values etc. You specify the CSS property a time frame and the easing mechanism which determines the how the speed of the transition is balanced. Ease in and out for example slowly ramps to a peak, then slows down as the transition ends.
<div class="container" style="padding: 40px">
<button id="Trigger2">Trigger FadeIn/FadeOut</button>
<div id="Fader" class="fadeout">
Hello World Text
</div>
</div>
And it’s then super easy to simply add and remove the appropriate styles using either other CSS classes or explicit script triggers. Here’s an example using a code trigger using jQuery:
$("#Trigger2").click(function () {
if ($("#Fader").hasClass("fadeout"))
$("#Fader").removeClass("fadeout").addClass("fadein");
else
$("#Fader").removeClass("fadein").addClass("fadeout");
});
And voila you have a fairly generic fade-in/fade-out transition that is essentially declarative – simply by using a couple of CSS classes. If you’re using a binding framework like AngularJS, Knockout, Ember etc. you can bind these classes directly to affect behavior, otherwise a couple of lines of JS code will do the trick.
You can animate a ton of CSS attributes in this way and it’s pretty straight forward to do this with most of them.
Height and Width Animations are more complex
You can also animate height and width using CSS transitions and the logic to set this up is the same. But there are a number of added complexities when using height and width animations because DOM elements are made up using the DOM Box model which projects the element’s width and height, plus the padding and borders etc. While it’s easy to animate height and width, to animate them to become invisible and the resize themselves to the proper size is actually a not as obvious as it seems.
I’m going to skip all of my false starts here – suffice it to say that some of the obvious animation paths using height and width directly didn’t work very well, because in order to hide the element I have to remove the padding. Then when making it visible the padding has to be added back in, but when the padding is added it the padding expands immediately, bypassing the animation transition. If the height is greater you’d see the padded element immediately followed by the remainder rendered with the transition. No good.
The better solution is to use the max-height property along with the overflow-y property (or max-width and overflow-x) to limit the sizing and removing any padding and margins by wrapping the element to be animated into another element that doesn’t have either padding or margins.
Here’s what this looks like in an example (and you can check out the example on Plunker):
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<style>
#Slider {
}
#Actual {
background: silver;
color: White;
padding: 20px;
}
.slideup, .slidedown {
max-height: 0;
overflow-y: hidden;
-webkit-transition: max-height 0.5s ease-in-out;
-moz-transition: max-height 0.5s ease-in-out;
-o-transition: max-height 0.5s ease-in-out;
transition: max-height 0.5s ease-in-out;
}
.slidedown {
max-height: 60px ;
}
</style>
</head>
<body>
<div class="container" style="padding: 40px">
<button id="Trigger">Trigger Slideup/SlideDown</button>
<div id="Slider" class="slideup">
<!-- content has to be wrapped so that the padding and
margins don't effect the transition's height -->
<div id="Actual">
Hello World Text
</div>
</div>
</div>
<script src="scripts/jquery.js"></script>
<script>
// slideup/slidedown
$("#Trigger").click(function () {
if ($("#Slider").hasClass("slideup"))
$("#Slider").removeClass("slideup").addClass("slidedown");
else
$("#Slider").removeClass("slidedown").addClass("slideup");
});
</script>
</body>
</html>
There are few tricky things going on here to make this work.
First there are the .slideup and .slidedown CSS classes with slideup being the closed mode. Note they basically set up the max-height and the transition. In closed mode max-height is set to 0 and overflow-y hidden which effectively hides all content. The slidedown class then sets the height to a specific height. Unfortunately you have to use a specific height here and that height has to be big enough to contain the largest content you expect to show and the size should be as close as possible to the size of your actual content. The animation runs to the max-height specified – if you’re content is smaller than the max-height the animation keeps going after your content has been shown. If it’s too big it the content will be cut off. Choosing the right size here is key.
You could also trigger the max-height via code if you need to calculate, but that’s not easy if the content is hidden or collapsed ($.outerHeight() returns 0 until the content actually resizes! More on this in the next section).
The next tricky thing is that you need to wrap your content – if you have any padding or margins. The wrapper effectively removes the padding from the wrapping container so that max-height only applies to the calculated height of the element. If you have no padding or margins then your content can go directly inside of the animated container. Otherwise the container guarantees the the wrapped element is completely hidden.
Finally the code to actually trigger the animations simply removes one class and replaces it with the other to invoke the effect.
This works well and easy to understand. The only downside is the max-height setting which has to be set to an explicit pixel value. We’ll address that in the next iteration.
A jQuery Plug-in to make it more reusable
Let’s take another look and see how to make this a little bit easier by building a small jQuery plug-in – jquery.slide-transition.js. This plug-in basically wraps the concepts described in the previous section and additionally provides a workaround for the max-height issue, by calculating the wrapper height so that we don’t have to guess at the size.
Here’s how the plug-in works. There are a couple of things that need to be done to use it:
- Add and configure a couple of CSS classes
- Wrap your content into a wrapping container
- Use the jQuery plug-in to trigger slide operations
First you need to define two styles:
<style>
.height-transition {
-webkit-transition: max-height 0.5s ease-in-out;
-moz-transition: max-height 0.5s ease-in-out;
-o-transition: max-height 0.5s ease-in-out;
transition: max-height 0.5s ease-in-out;
overflow-y: hidden;
}
.height-transition-hidden {
max-height: 0;
}
</style>
You can customize the transition to suit your needs with longer/shorter times and the easing mechanism of your choice.
You also need to wrap your actual content you want to slide up and down into a container and then apply one or both of these styles. The latter can be used to initially hide your content.
<div id="SlideWrapper" class="height-transition height-transition-hidden">
<!-- content has to be wrapped so that the padding and
margins don't effect the transition's height -->
<div id="Actual">
Your actual content to slide down goes here.
</div>
</div>
Here the #Actual element is your content which you can format as needed. #SlideWrapper is the added container that the transitions are hooked up to. As discussed previously, this wrapper is necessary to avoid the padding/margin/border size problems. I decided to let you create this element yourself, rather than have the plug-in create it as you can declaratively hide the element, without any tricky logic required to infer when to show or hide the inner content. The inner content is never touched by the plug-in, and the outer wrapper is what deals with the transition.
Additionally the plug-in also deals with the max-height sizing issue. Internally the plug-in briefly lets the container expand to get the real size of the element and then sets the max-height to match. This means you don’t have to specify ‘fixed’ max-height as before – the plug-in automatically figures out the right height which fixes the main nit I had with the original approach.
You can then trigger the plug-in with code like the following (example on Plunker):
<script>
$("#Trigger").click(function () {
if ($("#SlideWrapper").hasClass("height-transition-hidden"))
$("#SlideWrapper").slideDownTransition();
else
$("#SlideWrapper").slideUpTransition();
});
</script>
Here’s the complete plug-in code:
(function ($) {
/*
jquery.slide-transition plug-in
Requirements:
-------------
You'll need to define these two styles to make this work:
.height-transition {
-webkit-transition: max-height 0.5s ease-in-out;
-moz-transition: max-height 0.5s ease-in-out;
-o-transition: max-height 0.5s ease-in-out;
transition: max-height 0.5s ease-in-out;
overflow-y: hidden;
}
.height-transition-hidden {
max-height: 0;
}
You need to wrap your actual content that you
plan to slide up and down into a container. This
container has to have a class of height-transition
and optionally height-transition-hidden to initially
hide the container (collapsed).
<div id="SlideContainer"
class="height-transition height-transition-hidden">
<div id="Actual">
Your actual content to slide up or down goes here
</div>
</div>
To call it:
-----------
var $sw = $("#SlideWrapper");
if (!$sw.hasClass("height-transition-hidden"))
$sw.slideUpTransition();
else
$sw.slideDownTransition();
*/
$.fn.slideUpTransition = function() {
return this.each(function() {
var $el = $(this);
$el.css("max-height", "0");
$el.addClass("height-transition-hidden");
});
};
$.fn.slideDownTransition = function() {
return this.each(function() {
var $el = $(this);
$el.removeClass("height-transition-hidden");
// temporarily make visible to get the size
$el.css("max-height", "none");
var height = $el.outerHeight();
// reset to 0 then animate with small delay
$el.css("max-height", "0");
setTimeout(function() {
$el.css({
"max-height": height
});
}, 1);
});
};
})(jQuery);
Not a whole lot to this plug-in code. .slideUpTransition() simply applies a style and sets the max-height. .slideDownTransition() removes the hidden style and then captures the actual height of the element and explicitly sets the max height to that value. This ensures that the element sizes itself properly without the use of fixed height values.
Watch out for dynamic Elements
The above example works great, but when I actually moved code like this into my application show in the animated GIF at the beginning of the article I ran into another little snag. Specifically the slide down animation wasn’t working, while the slide up animation was - weird, right? Same logic after all, so why is this failing?
It turns out that it’s caused by the dynamically attached element that shows the item drop down toolbar. In the history item list I have one static ‘drop down toolbar’ which is moved and attached to the active item every time the user clicks or touches one of the items. IOW, the item is dynamically being moved around and attached to the active item in the DOM tree.
The problem is that the element is not in place when I call .slideDownTransition() so effectively the items weren’t visible yet. When the class tried to resize it’s still effectively hidden and so the CSS transitions had no effect. When the element finally renders it simply renders as a simply displayed object.
The workaround is to slightly delay applying the CSS classes with setTimeout() which allows the element to be put into place first, before the animations are triggered:
$scope.showOptions = function ($event) {
var $el = $($event.target).parents(".history-item"); // item
var $opt = $("#SlideWrapper"); // ‘toolbar’
if (!$opt.hasClass("height-transition-hidden"))
$opt.slideUpTransition();
else {
$opt.insertAfter($el); // attach
// have to delay for element to register in place
setTimeout(function () {
$opt.slideDownTransition();
},20);
}
};
Ah yes, setTimeout() – the cure all for DOM timing issues :-)
I suspect this is a common use case for slide down operations, so this is something to keep in mind. Elements that contain transitions have to be visible otherwise the transitions are not applied.
Is it worth it?
All of this seems a bit of work, but the end result is pretty straight forward. If you’re dealing with simple CSS transitions like Fading, using them is a no brainer and you should consider them as a replacement for jQuery’s fadeIn() and fadeOut(). For height and width transitions you need a little more effort to deal with the container sizing, but with the help of the plug-in I described it’s easy to push this into an application and use it almost as easily as the jQuery .slideUp() and .slideDown() features with much smoother results.
Resources
Other Posts you might also like