Grow your CSS skills. Land your dream job.

Stairway Navigation (A jQuery Plugin?)

Published by Chris Coyier

On a whim the other day I thought I'd build out an idea for navigation I had. It's nothing but a visual effect in which the hovered (or active) navigation item becomes the tallest "stair" and other items before and after it step down. It's simple, but it's not something you see very often. Probably for one major reason: you can't select "previous" elements in CSS.

View Demo

You can select "next" elements in CSS. You'd use the general sibling combinator to get all the next elements or the adjacent sibling combinator to get the very next one (which you could chain). Neither of those allow you to get the previous elements which, as you can see by the image above, is needed to do this effect justice.

In pseudo code, we're trying to do this:

/* Not Real Code */

a:hover { /* top stair stuff */ }

a:hover+1 { /* second stair stuff *}

a:hover-2 { /* third stair stuff *}

Rather than get too tricky for our own good with CSS, let's rely on a technology that can select previous elements: jQuery. jQuery has a .prev() function (and a few other related functions) that we can use to get what we need. Our psuedo code would become more like this real code:

$("nav a").hover(function() {

Presumably we'd clear all classes on all nav elements on a mouseleave event as well. That means to be most efficient we'd already have a pointer to all those elements.

var navEls = $("nav a");

  .on("mouseenter", function() {
     // add classes as above
  .on("mouseleave", function() {
     navsEls.removeClass("stair-1 stair-2 stair-3");

So now that we have a set, we might as well get a bit more efficient. Using .next() and .prev() means a lot of jQuery going back out to the DOM to figure out what to select (I think, correct me if I'm wrong there). Rather than do that, we can just select based on the set we already have selected based on it's position within that set. The .index() function helps us figure that out and .eq() let's us grab the element based on its index.

  .mouseenter(function() {


    var index = $(this).index();



That'll do the trick.

CSS does the design work

Notice that all the jQuery is doing is adding and removing classes. That's how UI and JavaScript should hang out the majority of the time. JavaScript is in charge of knowing about and changing states - CSS does the work of making the page look different.

The entire "stairway" visual effect comes in now, when we apply styles based on those classes.

.stair-1 {
  box-shadow: 0 0 10px rgba(black, 0.75);
  z-index: 3;
.stair-2 {
  box-shadow: 0 0 10px rgba(black, 0.5);
  z-index: 2;
.stair-3 {
  z-index: 1;

The "top" stair (stair-1) enlarges, moves to the right, and has a deep shadow. Each of the subsequents stairs does a bit less of all those things. You could also change colors here or do anything else that would make sense to your own application.

A jQuery Plugin?

I put those words in the title of this post because I think this is interesting territory.

Does this kind of thing "deserve" to be pluginized? For one thing - this is heavily dependent on CSS. Calling it a "Stairway Navigation" plugin isn't descriptive of what the actual jQuery code is doing. It also doesn't make use of any of jQuery's built-in features that are built for this, like it's ability to animate things - we instead leave that to CSS.

Anyway - we are going to pluginize it because it makes things more interesting.

Plugin Options

We'll make it the simplest set of options possible: how many stairs do you want stepping down? You'll call it on a navigation element that contains only anchor links:

  stairs: 2

Then in the plugin, we make sure we have access to a "stairs" variable that is either the passed in value or some default.

$.fn.stairwayNav = function(options) {
  var defaults = {
     stairs: 3
  this.options = $.extend({}, defaults, options);
  var stairs = this.options.stairs;

I love that pattern. It means we don't have to do any fancy crap checking if the object contains certain keys and ensuring they aren't blank and blah blah. If you pass in a value for "stairs", that's what ends up in the options object. If you don't, it gets a default value. Cool.


To honor that option, we now just run a little for loop as many times as there are stairs. We adjust the index value the more iterations it runs, just never selecting negative values.

  .mouseenter(function() {
    var index = $(this).index(), i, bef, aft;
    for(i = 1; i < stairs; i++) {
      bef = index - i;
      aft = index + i;
      allLinks.eq(aft).addClass("stair-" + (i+1));
      if (bef > 0) {
        allLinks.eq(bef).addClass("stair-" + (i+1));

Stairway Navigation Demo

Here is the demo on CodePen.


  1. Awesome! Something like stroll.js on :hover :D

  2. littleguy
    Permalink to comment#

    First demo link is wrong. Something with bears…

  3. Permalink to comment#

    Interesting indeed! The examples at the end seems to have the first one-two elements kinda jumpy in FF 16 (if I hover the second link, the first is not “stairy” as the second is when on the next links). Donno if it’s a problem of FF or there is another issue. Looking forward to using this soon.

  4. I think in the good old Flash days it was called “tsunami”, not “stairway” no ?

  5. Nathaniel
    Permalink to comment#

    This has to be one of the coolest things I have EVER SEEN !!!

  6. JohnMotylJr
    Permalink to comment#

    Very nice piece of code Chris.

  7. Permalink to comment#

    i click the “view demo” in my RSS reader, the link was change to “”

  8. Craig
    Permalink to comment#

    Pretty, but misleading.

    The steps in the ‘stair’ implies a navigational hierarchy that doesn’t exist.

  9. Dan
    Permalink to comment#

    Great !!! Love css-tricks :)

  10. Sir, Could you please suggest some e-books for CSS and Java Script .

    Thanks for Reading my comment out of the context.

    • Permalink to comment#

      Hi there, even though I am not the contextual “sir” you are addressing this question to, I’ld like to give u a title of a CSS3 resource book I found very useful:
      “CSS3: Visual QuickStart Guide” (by Jason Cranford Teague) and I guess there is an e-book version of this one available online as well (check amazon or the likes for more info).

      Can’t help you with javascript though. Let’s hope some other out of context sirs answer your question ;)

    • I’m not the contextual “sir” you’re after either, but I’m pretty sure I know what Chris would write if he saw your comment. I think you’re looking for his bookshelf.

  11. Permalink to comment#

    Very nice feel to it, might come in handy some day where appropriate, for now it is a nice gimmick indeed ;) fun to play around with and inspire the code-minded.

  12. DED
    Permalink to comment#

    In HCI this is often referred to as fish-eye navigation. Mostly used when you have a large list. The non-selected elements would be smaller than you have though. If I remember correctly from my classwork, this kind of navigation has had some success.

  13. steelpulsefan
    Permalink to comment#

    This will fit right in with any theme about Piano’s and stuff. Also, I love Martin Antsy’s line “I’m not the contextual “sir” you’re after either” Line of the year for comments world wide. :-D

  14. If stairs is set to anything above 4, the additional classes (active-5, active-6 and so forth) aren’t removed upon mouseleave.

    Here’s a quick and dirty regex to use instead:

    .mouseleave(function() {
        allLinks.prop('class', function (i, v) {
               return v.replace(/\bactive-\d+\b/, '');

    If a regex makes you feel all dirty inside, you could always build that classes string with a loop instead.

  15. Ryan Plas
    Permalink to comment#

    Seems like the first link never changes other than when you hover over it specifically.

    Not sure what’s causing that but just thought I’d point it out so someone smarter than me could check it out.

    • Ryan Plas
      Permalink to comment#

      Maybe I should take two seconds to actually mess around with something before commenting.

      Looks like a quick change fixes it.

      In the block:

      allLinks.eq(aft).addClass("stair-" + (i+1));
          if (bef > 0) {
      allLinks.eq(bef).addClass("stair-" + (i+1));

      If you change if (bef > 0) to if (bef >= 0) it works.

      I forked the codepen and made the change here.

  16. Permalink to comment#

    Great from an “Ohh pretty” perspective but from a usability standpoint it can be quite problematic. The first one is fine, though a bit over the top to define a link, the other ones are a problem though. The problem is that it becomes difficult to tell which link you are actually selecting.

    Users might instantly ask themselves several questions that can lead to confusions:

    “Why are these other links popping up when I select one of them?”
    “If I click this link is it going to send me to the page I actually want?”
    “Are these that are popping up related in some way that I don’t understand?”

    In the end I might use the first example but I would never use the others simply because it doesn’t favor the user.

  17. If you approach the buttons with your pointer from the left, the translateX transform moves the element away from the pointer which fires the mouseLeave event function. I’ve had this problem on stuff I’ve built before and I’ve tried some pretty elaborate stuff to fix it.

    I guess you could wrap each menu item in an element and add padding to instead of using the translateX transform.

    • I can confirm it’s behaving like that, but that’s super weird. Translate transforms (really any transform) aren’t supposed to really “move” the element. The space left behind is still the space the element actually occupies in the flow. A bug, I guess.

    • That’s interesting that the space left behind should be considered part of the element in spite of contradictory behavior. I checked in a few current browsers and it happened in each of them. Joseph Silber made a great suggestion in this comment which I think can be applied not only here but to many other cases where I’ve had similar problems.

      He’s firing the mouseleave function upon vacating a parent element. That’s way simpler than starting and killing timers the way I usually do to smooth out hover functions.

    • I don’t think this is a bug. Transforms, like relative-positioned elements, don’t alter the page layout, but the events will only fire where the element is actually painted.

      See here for how relatively-positioned elements work. I think this is the expected behavior for transforms too.

    • Makes sense. So the mouseleave fires, but :hover would still be “working” (so you could click the link).

    • @chris …which is exactly what I didn’t say.

      Neither events nor the hover pseudo-class will be applied. All it does is keep a hole in the document flow, so that it doesn’t affect any other elements on the page. For all other purposes, it is comfortably located in its new home.

    • OK right. Here you can see the hole that is left from a floated link but you can’t click there – the area that responds to any events is the red square where it has been translated to.

  18. Permalink to comment#

    Damn that looks good. Wish I could actually use this for a project.

  19. For all those complaining about the flickering effect when approaching the links from the left: the solution is to simply remove the active-* classes onmouseleave of the parent instead. You then also have to remove them at the begining of mouseenter, so in this pen I’m using a function to handle that.

  20. It’s a really cool idea and it shows off another sweet technique that jQuery has to offer; however it’s not the sort of thing I’d really use.

  21. Thad
    Permalink to comment#

    Nice work as usual. Please keep up the great work.

  22. Permalink to comment#

    Another great tut, Chris. Over the years I’ve learn from you … thanks again keep rocking!

  23. Permalink to comment#

    Well done. I’d like to tweak it and give it a try using an unordered list.

  24. Neal Koss
    Permalink to comment#

    I am an amateur at this, but when I run Sass on the SCSS file, I get an error on the @include transform(translate3d(0, 0, 0));
    line. Probably would be an error on each of the other @include also. What am I doing wrong here?

  25. Neal Koss
    Permalink to comment#

    OK, so I installed Compass and I see I have to import the functions – transform and transition. I put the following statements at the top of my main.scss…
    @import “compass/css3/transform”;
    @import “compass/css3/transition”;

    That seems to be what the docs are suggesting. Then in my command line (I am using a Win 7 PC), I run…
    sass-convert sass/main.scss sass/main.sass

    and that ran with no errors. Then…
    compass compile sass/main.sass

    and that, too ran with no error, stating that it “overwrite stylesheets/main.css”. That really sounded good. Unfortunately, the file, “main.css” remained empty. What am I missing here?

  26. Neal Koss
    Permalink to comment#

    Forget previous. I figured out the problem. If I have further difficulties, I’ll be back…thanks

  27. Ajay Gupta
    Permalink to comment#

    How to add js code to my HTML page, I have already added but it is not working. Please tell me the procedure to do the same

    • Neal Koss
      Permalink to comment#

      @Ajay and @Chris….Same problem. I figured out how to translate the SCSS, but now the JS doesn’t seem to do anything. I’m glad it’s not just me.

  28. Cvetlov
    Permalink to comment#

    Guys who knows how to get the normal menu displayed in Internet Explorer?

  29. Permalink to comment#

    The SCSS seems broken… or maybe a Codepen glitch?

  30. Neal Koss
    Permalink to comment#

    I would love it if someone could respond and help us poor souls who would like to actually get this menu working. There must be some error somewhere….does anyone know what it is?

  31. Permalink to comment#

    Hey #Neal,
    I did it like this:
    I jumped over to code pen and copied the three items into three separate files stairway.html, stairway.js and stairway.scss ( using sublime with a syntax highlighter{ i’ve just started getting into sublime as of today I think i’m moving from my trusty notepad++ thanks to one of Chris’s presentations})
    Since i’ve never delved into scss or sass and i’m not on my box running ruby but on windows 7.
    I found a GUI compiler called scout app:

    I then added the followwing lines to the top of my stairway.scss file:
    @import “compass/css3″;

    @import “compass/css3/transform-legacy”;

    in the configure tab of scout I choose the same folder to input as to output saved my .scss file and once scout app is running it automatically compiles the .css file.
    since i wanted to test this on wordpress and i know wordpress uses non-conflict jQuery i changes all the instances of $ to jQuery in the stairway.js file.
    added the tag to the header and just added the js between two tags, and wrapped it in
    jQuery(document).ready(function(){ //stairway.js script is copied and pasted here replacing this //});

    and it seems to be working ok for me more or less ( i think it might need a little tweaking but the basic is there) ..
    I have a version of the compiled scss here if you want to download it:


    and a working version of the menu on the front end of that site.
    Goodluck Neal
    if you need anything else just leave me a comment here and i’ll try to help.

    • Neal Koss
      Permalink to comment#

      Thanks for trying. I tried Scout, but it just gave me an Adobe Air 3214 error. But I already was able to do the translation and I could never get to your CSS file. That link just didn’t work. Take a look at mine at The files are index.html, js/stair.js and css/main.css. The menus show up, but the stairs do not move.

      As an aside, do you think Sublime is better than using Dreamweaver?

  32. Permalink to comment#

    I think sublime is better than dreamweaver!!
    have you called jQuery please read this:

    chrome dev tools is throwing up 1 $ undefined error..

    • Neal Koss
      Permalink to comment#

      EGAD!! It is always amazing how someone (ME!!!) can forget the simplest things. Of course, that was the problem and it is working now. Thanks for the link, but I realized it as I was reading your first note “…called jQuery…”. I appreciate your persistence.

  33. Permalink to comment#

    you need to add this between the tags:

This comment thread is closed. If you have important information to share, you can always contact me.

*May or may not contain any actual "CSS" or "Tricks".