Handling CSS Transitions with prepareTransition

Using CSS transitions can be quite fun. But what's not fun is when you want to transition something that needs to use display:none or visibility:hidden (or really, any non-transitionable property).

For example, let's say you have a dialog. When the user clicks on the close button, you want the dialog to fade out — a simple transition from opacity:1 to opacity:0. The problem is that the element is still there, even though you can't see it. There's the transitionEnd event that you can use in JavaScript to set display:none at that time but that doesn't help you for browsers that don't support transitions.

I put together a small little jQuery plug-in called prepareTransition to help out and is available on Github. Feedback is most welcome.

Example Usage

Let's say you had a dialog that you wanted to hide when someone clicks the close button.

.dialog {
    position: absolute;
    /* and other dialoggy styles */
}

.is-hidden {
    display: none;
}

// and our jQuery:
$(".btn-close").click(function(){
    $(".dialog").addClass('is-hidden');
}) 

Now let's layer on some CSS transitions.

.dialog {
    position: absolute;
    /* and other dialoggy styles */
    opacity: 1;
    transition: opacity 1s; /* don't forget vendor prefixed */ 
}

.is-hidden {
    display: none;
    opacity: 0;
}

// and our jQuery:
$(".btn-close").click(function(){
    $(".dialog").addClass('is-hidden');
}) 

In browsers that don't support transitions, this will still work but for browsers that do support transitions, this doesn't work. Why is that? The display property ends up removing the element from flow before the animation even starts. Clearly less than ideal.

As mentioned at the beginning of this article, I could remove display: none from is-hidden and then use the transitionEnd event to add it back in but then I don't have something that works in browsers that don't support transitions.

The prepareTransition method forces display: block; until the end of the transition. It does this by applying an is-transitioning class to the element and then using the transitionEnd JavaScript event to remove the class from the element.

.dialog {
    position: absolute;
    /* and other dialoggy styles */
    opacity: 1;
    transition: opacity 1s; 
}

.is-hidden {
    display: none;
    opacity: 0;
}

.is-transitioning {
    display: block !important;
    visibility: visible !important;
}

// and our jQuery:
$(".btn-close").click(function(){
    $(".dialog").prepareTransition().addClass('is-hidden');
}) 

Using prepareTransition is a handy way of allowing an easy fallback design for browsers that don't support transitions while making it easier to manage transitions for browsers that do support it.

Added to the specification

This would be much easier if there was a transitionStart event, though. Then, a method wouldn't have to be run before applying the is-transitioning class.

Even better would be a pseudo-class that could be applied to an element.

:transition {
    display: block !important;
}

Or maybe it's just assumed that any element in transition should be display: block (and visibility: visible). In any case, we clearly need a little more at the browser implementation level to simplify this use case.

Published January 12, 2012
Categorized as JavaScript
Short URL: https://snook.ca/s/1010

Conversation

22 Comments · RSS feed
Josh King said on January 12, 2012

Interesting way to go about it. Thanks.

Pat Cavit said on January 12, 2012

The YUI 3 Transition module provides both a start & end method for you as well as falling back to timer-based animations for numeric properties. It's really awesome.

Patric Jansson said on January 12, 2012

How does this correlate to jquerys fadeOut() for example?

Stephen Tudor said on January 12, 2012

I like your transitionStart idea, as it leaves the door open for a transitionEnd callback. CSS transitions could really use this.

Jonathan Snook said on January 12, 2012

@Patric Jansson: fadeOut will certainly fade just as well as my example. This is reflective of my move towards using JavaScript more for describing state change (by adding and removing classes) and leaving it up to CSS to handle the styling of that state.

Chris Coyier said on January 12, 2012

Here's some similar thinking on the subject by Florent Verschelde:

http://fvsch.com/code/transition-fade/

Matt Reed said on January 12, 2012

If I'm understanding you correctly, this is similar to how the Twitter bootstrap pulls off the same effect, but they do it a little differently. They use the following (LESS):

.fade {
.transition(opacity .15s linear);
opacity: 0;
&.in {
opacity: 1;
}
}

But I still hate that we have to get JS in the mix.

Ryan Rahlf said on January 12, 2012

I think the issue with tracking transitions with transitionStart or :transitioning is that you don't know which transition is working. You could have an element with two classes, one with a 1s transition of color and one with a 3s transition of scale, for example.

Jonathan Snook said on January 12, 2012

@Ryan Rahlf: you know which transition the event fired on with event.srcElement. And :transition is a pseudo-class that could be applied to a sepecific element like .dialog:transition { }.

Mike Gossmann said on January 12, 2012

Spec-wise, I'd love it if transitions actually handled non-animating properties instead of ignoring them. If you include something like display in the list of properties to transition, it should know that even though it can't go smoothly from one state to the other, it should jump at the end instead of the start. Even if it just respected the delay options, this would be all you need.

madr said on January 13, 2012

This was a good read, thank you. I myself has solved some similar issues by using applying the opacity transition first and a minimal setTimeout() to delay .is-hidden.

Andrew said on January 14, 2012

If I've understood the problem correctly... I think you can achieve the fade out affect and hiding the element, with a width and height of zero, overflow hidden, and positioned negatively, off-screen. avoiding the need for display none.

Jonathan Snook said on January 14, 2012

Andrew: The problem isn't in hiding the content but having the ability to push certain properties to be applied at the end of the transition for browsers that support it.

Ryan Rahlf said on January 16, 2012

Sorry Jonathan, my explanation could have been more clear. I was thinking about multiple properties being transitioned on the same element but with different durations. An example from http://www.webkit.org/blog/138/css-animation/ :

div {
-webkit-transition-property: opacity, left;
-webkit-transition-duration: 2s, 4s;
}

In this case "opacity" would stop transitioning in 2 seconds, but "left" would stop in 4 seconds. At what time would the :transitioning pseudo-class be removed? What if each of these transitions were under different selectors, but applied to the same element, such as

<div class="transOpacity transPosition">

.transOpacity {
-webkit-transition-property: opacity;
-webkit-transition-duration: 2s;
}
.transPosition {
-webkit-transition-property: left;
-webkit-transition-duration: 4s;
}

Different start, end, and duration of property transitions can really at a lot of richness to animation when done well. I'd be against a spec change if it limited our ability in that regard.

Just my thoughts on the pseudo-class suggestion.

Jonathan Snook said on January 16, 2012

@Ryan: different end times aren't handled by this script and that is something I'll need to look into addressing.

As for the spec proposal, it would work in the sense that the pseudo-class would always be applied until all transitions were complete but wouldn't really differentiate between the various transitions—just that the element is still in transition. So, in your example, div:transition would be applicable for 4s.

The quickest thing that comes to mind is div:transition(left) and div:transition(opacity) as potential ways of differentiating between the two.

Great feedback.

Brian Feister said on January 17, 2012

Brilliant Jonathan! I was using a chained animation that started with the element positioned off-screen and opacity 0. Animate in a chain - first bring it on-screen in a 0s transition and then do the rest of the animation as a later stage in the animation chain. This is much simpler! :)

Ryan Cannon said on January 20, 2012

If we're going to start changing the spec, let's change correctly. The simple solution is to allow non-transitionable properties not to change unit the transition is supposed to start. e.g. this CSS should be enough:

.dialog {
  transition-property: opacity, display;
  transition-duration: 0.5s, 0;
  transition-delay: 0, 0.5s;
}

Other than that, this seems like a fairly elegant solution.

Jonathan Snook said on January 21, 2012

@Ryan Cannon: Thankfully, they're looking to change to the spec to allow transitions between all elements and using step-start or step-end to decide when those values transition.

troy said on February 06, 2012

jonathan, with this approach how do you handling animations for legacy browsers? Do you use object detection and provide a javascript fallback or drop the animations all together for these browsers?

Jonathan Snook said on February 07, 2012

@Troy: The intention with this plug-in is to not provide a javascript animated fallback. It's best for simple transitions between states. If you wished to provide a javascript fallback, I'd use something like Modernizr instead to detect transition support.

NICCAI said on April 09, 2012

I was just thinking, css needs a pseudo-class for :animating.

Caleb Hearon said on June 28, 2012

Thanks, this is very useful. I was thinking a pseudo-class for :animation-complete or :transition-complete, but having :animating or :transitioning could achieve the same results I think.

Sorry, comments are closed for this post. If you have any further questions or comments, feel free to send them to me directly.