Tuesday, August 24, 2010

Pure CSS Dropdown Menus (with Transitions) (Part 1: Fade)

This blog post stems from a comment I left on onwebdev a couple of weeks ago.

If you don’t feel like reading it, basically, I argued that CSS transitions (part of the CSS3 spec) could be used in place of Javascript to create purely presentational behaviors like sliding or fading dropdowns.  I spent yesterday and today figuring out just how to implement those things.

Let’s start with a plain, standard CSS dropdown menu:

<ul class="nav"> <li><a href="#">Link 1</a></li> <li class="parent"> Submenu 1 <ul> <li><a href="#">Item 1</a></li> <li><a href="#">Item 2</a></li> </ul> </li> </ul>

And the CSS:

body { margin: 0; } .nav { margin: 0; padding: 0; background-color: rgba(0,0,0,.6); color: white; } .nav li { display: inline-block; font-weight: bold; } .nav li.parent { position: relative; } .nav li.parent, .nav a { padding: .5em 1em; color: white; } .nav li.parent:hover, .nav a:hover { background-color: rgba(0,0,0,.2); } .nav a { display: block; } .nav li.parent ul { display: none; position: absolute; top: 100%; left: 0; margin: 0; padding: 0; overflow: hidden; background-color: rgba(64,64,64,.98); } .nav li.parent:hover ul { display: block; } .nav li.parent ul li { width: 100%; }

This is the basic dropdown you’ve all seen before (Demo).  Hover over a menu item, and it changes color, and a dropdown appears, instantly.  Stop hovering, and the dropdown disappears, just as instantly.  But what if we don’t want it to appear instantly?  What if we want it to appear with a gradual fade?

It’s a little bit more CSS—CSS Transitions, to be exact (And I’m feeling lazy, so we’ll do Webkit only).  First, we have to change how we’re making our submenus appear and disapper, since display can’t be transitioned.

Instead, we’ll set a height of 0, and an opacity of 0 for the hidden state, and a height of auto and an opacity of 1 for the shown state.

.nav li.parent ul { position: absolute; top: 100%; left: 0; margin: 0; padding: 0; overflow: hidden; background-color: rgba(64,64,64,.98); height: 0; opacity: 0; -webkit-transition: opacity .25s ease-in; } .nav li.parent:hover ul { height: auto; opacity: 1; }

Without the -webkit-transition line, this acts exactly like our original dropdown.  The magic comes with the transition property (Demo).

This transitions just the opacity, so the heights take effect immediately.  The upside of this is that when you hover over where the menu is going to be, it doesn’t appear.  The downside is that it disappears immediately (because the height is 0).  If you leave the height off, you get a nice fade out, but the submenu is always there, just invisible, so you can still hover over it by accident.

I haven’t yet come up with a perfect solution to this, but while trying, I found something pretty cool (Demo).  We can have two transitions combined, at least on the way out.  Here’s the code:

-webkit-transition: all .25s ease-in; nav li.parent:hover ul { height: auto; min-height: 2em; opacity: 1; }

“All” makes anything transition that can, whether it’s specified or not.  That’s useful in this case.  We get the same fade in behavior as before, but additionally, we fade out and scroll up to disappear.  Here’s how the scroll works.

Transitions apply to height, but not with values of auto.  So when we unhover, the height immediately becomes 0.  But we’re still in transition, so the min-height of 2em from the hover takes effect.  And then, since our finishing state has an implicit min-height of 0, the min-height changes gradually to 0, causing the submenu to wipe up.

Next (probably not tomorrow, but maybe Thursday): Part 2: Drawers.