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.

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,
a:hover+1 { /* second stair stuff *}
a:hover-2,
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() {
$(this)
.addClass("stair-1")
.prev()
.addClass("stair-2")
.prev()
.addClass("stair-3")
.end()
.end()
.next()
.addClass("stair-2")
.next()
.addClass("stair-3");
});
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");
navEls
.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.
navEls
.mouseenter(function() {
$(this).addClass("stair-1");
var index = $(this).index();
allLinks.eq(index+1).addClass("stair-2");
allLinks.eq(index-1).addClass("stair-2");
allLinks.eq(index+2).addClass("stair-3");
allLinks.eq(index-2).addClass("stair-3");
})
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 {
transform:
scale(1.10)
translateX(24px)
box-shadow: 0 0 10px rgba(black, 0.75);
z-index: 3;
}
.stair-2 {
transform:
scale(1.07)
translateX(12px)
box-shadow: 0 0 10px rgba(black, 0.5);
z-index: 2;
}
.stair-3 {
transform:
scale(1.04)
translateX(4px)
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:
$(".main-nav").stairwayNav({
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.
Looping
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.
navEls
.mouseenter(function() {
$(this).addClass("stair-1");
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.
Awesome! Something like stroll.js on :hover :D
First demo link is wrong. Something with bears…
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.
I can confirm both the jumpgy behavior (Place your mouse where the lettering starts on the navigation elements in it’s unexpanded – or should I say unstaired? – state) as well as the first element not being selected (stairified, class added or what have you) When the second element is selected. Now I’m no wizard, but I think if you change your line 21 (in the CodePen demo)
to
then that should do the trick.
I really do like the plugin though, and I also have to give props to CodePen, I took almost 5 minutes trying to get the sourcecode in a way I could easily test and edit it, before I figured I could just modify it on CodePen.
I hope this is helpful!
I think in the good old Flash days it was called “tsunami”, not “stairway” no ?
This has to be one of the coolest things I have EVER SEEN !!!
Very nice piece of code Chris.
i click the “view demo” in my RSS reader, the link was change to “http://codepen.io/chriscoyier/pen/sdeuk”
Pretty, but misleading.
The steps in the ‘stair’ implies a navigational hierarchy that doesn’t exist.
Great !!! Love css-tricks :)
Sir, Could you please suggest some e-books for CSS and Java Script .
Thanks for Reading my comment out of the context.
:)
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.
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.
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.
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
If
stairs
is set to anything above4
, the additional classes (active-5
,active-6
and so forth) aren’t removed uponmouseleave
.Here’s a quick and dirty regex to use instead:
If a regex makes you feel all dirty inside, you could always build that classes string with a loop instead.
There’s a
g
flag missing from that regex.Oh, how I wish we could edit comments here…
Can you do that for me Chris?
Here’s a forked pen demonstrating a 6-lever-stair system, using the regex technique above:
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.
Maybe I should take two seconds to actually mess around with something before commenting.
Looks like a quick change fixes it.
In the block:
If you change
if (bef > 0)
toif (bef >= 0)
it works.I forked the codepen and made the change here.
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.
You can give it a more subtle look, as I’ve done in this pen, by using
skew
instead ofscale
.The effect looks more like a bulge than stairs, so that it doesn’t seem like the other ones are popping up so much.
“Favor the user” isn’t just one thing. What is you wanted the user to choose something like what flavor of cake they prefer and you want to minimize the influence the other choices have on their reaction to the one which is hovered.
Just a though. Speaking of cake … here’s a fork.
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 http://codepen.io/chriscoyier/pen/JLwqm but you can’t click there – the area that responds to any events is the red square where it has been translated to.
Damn that looks good. Wish I could actually use this for a project.
For all those complaining about the flickering effect when approaching the links from the left: the solution is to simply remove the
active-*
classesonmouseleave
of the parent instead. You then also have to remove them at the begining ofmouseenter
, so in this pen I’m using a function to handle that.Very nice! Simple and effective.
You can just remove the
translateX()
completely, as I did in my fork. My OCD thinks it’s more aesthetically pleasing.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.
Nice work as usual. Please keep up the great work.
Another great tut, Chris. Over the years I’ve learn from you … thanks again keep rocking!
Well done. I’d like to tweak it and give it a try using an unordered list.
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?
That’s a Compass mixin so make sure you’re using that too.
AHA! I see I’ve got a lot more to learn…Thanks for the tip
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?
Forget previous. I figured out the problem. If I have further difficulties, I’ll be back…thanks
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
@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.
Guys who knows how to get the normal menu displayed in Internet Explorer?
The SCSS seems broken… or maybe a Codepen glitch?
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?
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:
http://mhs.github.com/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:
http://buildawebdoctor[dot]com/wp-content/themes/twentytwelve/css/stairway.css
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.
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 http://www.nealkoss.com/stair. 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?
I think sublime is better than dreamweaver!!
have you called jQuery please read this:
http://docs.jquery.com/Tutorials:How_jQuery_Works
chrome dev tools is throwing up 1 $ undefined error..
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.
you need to add this between the tags:
http://docs.jquery.com/Tutorials:How_jQuery_Works