CSS3 transitions using visibility and delay
Apr 21, 2011One of the most common CSS3 transition animations used by developers in the coming years will undoubtedly be making elements appear and disappear via the
opacity
property. What previously was only possible using JavaScript can now be easily described in significantly less bulky CSS code:
div { opacity:0; transition:opacity 1s linear;* } div:hover { opacity:1; }
Sample: (hover your mouse below)
* Note: Because the spec is still in flux, most browsers require a vendor-specific prefix such as
-webkit-transition
for Chrome/Safari,
-moz-transition
for Firefox and
-o-transition
for Opera to make CSS3 transitions work. For the sake of simplicity in the article, I will be using just plain
transition
in the examples.
The most exciting thing about CSS3 transitions is that, by design, they naturally fall back to the normal non-animated transitions for browsers which don't support them. That means you can start using them in production designs today! Using
opacity
to fade in elements is great for all kinds of novel website effects which reduce reliance on JavaScript for things developers everywhere are already doing.
Take for example, the drop-down menu. For years developers have been creating snappy menus using only CSS
:hover
effects. It would only make sense to start applying for opacity transitions to these menus to make them just a little bit more slick. However, here is where we run into our first problem with using
opacity
.
An element with
opacity
of zero is still "opaque" to clicks and mouse-overs. This causes serious problems, especially for drop down menus:
div { background-color:#f6f6f6; padding:2px 5px; position:relative: } div > ul { list-style-type:none; margin:0px; padding:2px 10px; position:absolute; left:0px; top:100%; background-color:#eeeeee; opacity:0; transition:opacity 0.5s linear; } div:hover > ul { opacity:1; }
Sample:
- Menu item 1
- Menu item 2
- Menu item 3
- Menu item 4
It looks nice, but the problem becomes obvious when you try to click the link. The fully transparent menu still appears when we hover any area it covers, essentially preventing all interaction with any content beneath. Not to mention the behaviour itself is quite odd, especially when moving the mouse toward the link from below. What we want is to prevent the menu from receiving hover events when it is fully transparent.
Okay, the
display
property can do that with the
block
and
none
values. Let's give it a try:
. . . div > ul { . . . display:none; opacity:0; transition:opacity 0.5s linear; } div:hover > ul { display:block; opacity:1; }
Sample:
- Menu item 1
- Menu item 2
- Menu item 3
- Menu item 4
Hey now, we can click that link without the menu appearing! But... it no longer animates in Chrome and Firefox, and it only animates the fade-in part in Opera. When you mouse out of the menu, it just vanishes. Well, this sucks; I guess we can't use
display
with transitions if we want them to work reliably cross-browser.
Fortunately, there is another property we can use:
visibility
. Elements that are
visibility:hidden
are transparent to clicks and hover events, which is exactly what we want. Let's do this!
. . . div > ul { . . . visibility:hidden; opacity:0; transition:opacity 0.5s linear; } div:hover > ul { visibility:visible; opacity:1; }
Sample:
- Menu item 1
- Menu item 2
- Menu item 3
- Menu item 4
Excellent! Now all the browsers behave the same way. Unfortunately they all behave the same way as Opera: The fade-in works fine, but there is no fade-out, it just disappears. To debug this problem, we need to delve deeper into how CSS3 transition timing actually works.
We have an element, the menu, which switches from hidden to visible on mouseover, and smoothly transitions from fully transparent to fully opaque in half a second. That seems logical, but why is the fade-out not happening when we reverse that process? The key is knowing at what point the switch from
visibility:visible
to
visibility:hidden
occurs. In both cases, this switch happens immediately.
Fade-in action timeline
- User mouses over the element
-
visibility
is switched tovisible
-
opacity
transition animation begins -
opacity
transition animation ends
Fade-out action timeline
- User mouses out of the element
-
visibility
is switched tohidden
-
opacity
transition animation begins -
opacity
transition animation ends
In the second case, when
visibility
switches back to
hidden
, it sabotages our whole animation by hiding it. The animation still occurs, it's just hidden from view. We really want the fade-out action timeline to look like this:
- User mouses out of the element
-
opacity
transition animation begins -
opacity
transition animation ends -
visibility
is switched tohidden
But how in the world can we delay a style from taking effe... Oh my goodness, CSS3 transitions can do that too! And lucky us,
visibility
is one of the select group of CSS properties which are affected by CSS transitions. Let's slap on a
visibility
transition!
. . . div > ul { . . . visibility:hidden; opacity:0; transition:visibility 0s linear 0.5s,opacity 0.5s linear; } div:hover > ul { visibility:visible; opacity:1; }
Sample:
- Menu item 1
- Menu item 2
- Menu item 3
- Menu item 4
Quick explanation: We added a
visibility
transition with a
0s
animation time, a
linear
animation curve, and we also used the optional delay argument to set an animation delay of
0.5s
. This means there will be a half second delay before the transition takes place, which exactly equals the time taken by our
opacity
fade out. And...
... okay the fade-out works, but adding that code ruined the fade-in which was working so nicely before. What gives? The delay we added to the
visibility
property is taking effect both during the fade-in and the fade-out. All we've done is made our fade-in action timeline look like this:
- User mouses over the element
-
opacity
transition animation begins -
opacity
transition animation ends -
visibility
is switched tovisible
ARGH! Now what? We want the
visibility
to change with no delay on the fade-in, but with a half-second delay on the fade-out. Is that even possible? It seems like there is no way to specify different transition properties for the two halves of the animation effect. Is all lost? Must we resort to JavaScript?
There is one more thing we can try. Like in many CSS modules, such as
border
and
background
, the CSS3
transition
property is actually a shortcut property which groups several properties you can specify individually. One of these properties is
transition-delay
. Using this property we want to temporarily set the delay of the
visibility
transition to
0s
only during the fade-in portion of the animation. But how do we specify that?
Oh yeah! The fade-in only happens while the user has their mouse over the element. The fade-out doesn't happen until the mouse cursor is removed. So we can apply this special
transition-delay
to the
:hover
state of the element.
. . . div > ul { . . . visibility:hidden; opacity:0; transition:visibility 0s linear 0.5s,opacity 0.5s linear; } div:hover > ul { visibility:visible; opacity:1; transition-delay:0s; }
Sample:
- Menu item 1
- Menu item 2
- Menu item 3
- Menu item 4
You'll notice that we only specified one delay time, while we are using two transitions:
visibility
and
opacity
. According to the CSS3 specification, when there aren't enough delay values to go around, they are repeated to apply to all transitions. In this case, the
opacity
transition had a
0s
delay by default so it actually doesn't change.
How about that, it works! With a temporary change in the delay value of the transition, we successfully created a drop-down menu that both fades-in and fades-out very nicely, and also doesn't interfere with content beneath it. But is looking nice the end of the story?
Benefits for Usability
Heavens no! Drop-down menus using
:hover
have been blasted by usability experts for years; especially when you start adding in sub-menus. If you move your mouse just one pixel outside of an element in the
:hover
chain, the whole menu up to that point will collapse instantly, likely hiding the menu option you were trying to get to.
All that changes with CSS
opacity
transitions. When your mouse cursor travels outside a
:hover
chain element, its children don't disappear immediately, but rather start the
opacity
animation to transparency. Before the animation has completed, the element is still available to receive
:hover
events. Therefore if you move you mouse back onto the element quickly, the chain won't collapse and your target will still be available to select. This solves the dreaded "diagonal problem" that plagues so many CSS drop-down menu implementations.
Who knew CSS3 transitions could be for more than just fancy effects? They're a boon for menu usability as well! In the future, I definitely expect we will see many more uses for transitions beyond simply prettying up websites. Perhaps you are tinkering with some already.
⇐ Graphing the NHL 2010-2011 playoff race in HTML5 canvas | Dinosaur Comics ⇒ |
# Comment by Jake
thanks this helped a lot!
# Comment by Simone
Well, a very interesting article! Thanks!
But, I've a problem with webkit browser (both Chrome and Safari)...
Some animation run the "fadeout" effect (the same that happens on mouse out) immediately on the page load.
Only Firefox works as aspected.
I don't know why only webkit fails on this...
Thanks.
# Comment by Cory
Clever. Well done, sir.
Seems like a hackaround to something that should have been built into the spec... but hey, that's another story.
# Comment by Justin
Awesome!
Great walk through, well explained, very useful.
Thanks =)
# Comment by Brian
The only weakness of this approach is that for large elments that you want to hide and do a display toggle with css3 transitions, the lack of "display: none" results in scrollbars if the visually hidden element overflows the normal page wrapper. Any suggestions for resolving this? I think it bears including as an edit in your original post if there's a nice solution, still trying to think of one myself...
# Comment by Danny
Sweet! Hit this issue now with transitions, trying to animate display: none and failing. Very clever, especially like the walk-through problem/answer approach.