CSS3 transitions using visibility and delay

Apr 21, 2011

One 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)
Hello World!


* 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:
Hover for menu
  • Menu item 1
  • Menu item 2
  • Menu item 3
  • Menu item 4
I'm a link!


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:
Hover for menu
  • Menu item 1
  • Menu item 2
  • Menu item 3
  • Menu item 4
I'm a link!


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:
Hover for menu
  • Menu item 1
  • Menu item 2
  • Menu item 3
  • Menu item 4
I'm a link!


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

  1. User mouses over the element
  2. visibility is switched to visible
  3. opacity transition animation begins
  4. opacity transition animation ends

Fade-out action timeline

  1. User mouses out of the element
  2. visibility is switched to hidden
  3. opacity transition animation begins
  4. 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:

  1. User mouses out of the element
  2. opacity transition animation begins
  3. opacity transition animation ends
  4. visibility is switched to hidden

But how in the world can we delay a style from taking effe... Oh my goodness, CSS3 transitions can do that too! :O 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:
Hover for menu
  • Menu item 1
  • Menu item 2
  • Menu item 3
  • Menu item 4
I'm a link!


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:

  1. User mouses over the element
  2. opacity transition animation begins
  3. opacity transition animation ends
  4. visibility is switched to visible

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:
Hover for menu
  • Menu item 1
  • Menu item 2
  • Menu item 3
  • Menu item 4
I'm a link!


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.


Comments closed

Recent posts

  1. Customize Clipboard Content on Copy: Caveats Dec 2023
  2. Orcinus Site Search now available on Github Apr 2023
  3. Looking for Orca Search 3.0 Beta Testers! Apr 2023
  4. Simple Wheel / Tire Size Calculator Feb 2023
  5. Dr. Presto - Now with MUSIC! Jan 2023
  6. Archive