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.
Conversation
Interesting way to go about it. Thanks.
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.
How does this correlate to jquerys fadeOut() for example?
I like your
transitionStart
idea, as it leaves the door open for atransitionEnd
callback. CSS transitions could really use this.@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.
Here's some similar thinking on the subject by Florent Verschelde:
http://fvsch.com/code/transition-fade/
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.
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.
@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 { }
.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.
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
.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.
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.
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.
@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.
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! :)
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:
Other than that, this seems like a fairly elegant solution.
@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.
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?
@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.
I was just thinking, css needs a pseudo-class for :animating.
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.