DerekAllard.com

Conditionally Sticky Sidebar

If you use pretty much any browser except IE 6 (more on that later) when you visit this site, you've probably noticed that the dark-grey sidebar scrolls with you just until the banner and menu are off the page, and then locks itself into position or "sticks". When you scroll back to the top, if the menu and/or banner need to be seen again, the sidebar politely resumes its normal scrolling duties. Go ahead, try it now, I'll wait. Fun isn't it? I've had a number of people comment on that, so I thought I would outline how I accomplished it.

First of all, I'm not the first person to implement a "sticky" or "fixed" feature on my site, but I am the only one I know of that makes a sidebar (or any part of my site) sticky based on scrolling conditions (if you know of any other, please leave a comment for me). Making non-interactive sticky components is easily done using CSS 2.

#fixedDiv {
     position: fixed;
}

Now you just need a long scrolling page and ta-da, you're done. If all you wanted was a permanently sticky item, that would do it. For this site though, I didn't want it permanently sticky, I wanted it to toggle between sticky and scrolling behaviours depending on how far a visitor had scrolled down.

I accomplished this by using javascript to conditionally change the position of the sidebar div to "absolute" (normal scrolling) and "fixed" (lock into place). To make it happen, there are 3 separate items that need to be addressed.

  1. Triggering something to happen every time the user scrolls up or down the page;
  2. Figuring out how far from the top we are;
  3. Changing the sidebar so that it gets locked into position if we've scrolled far enough, and put back into the normal flow of the page if we're close enough to the top.

Since the position on the page is something that can change at any time, I needed a way to test for the new position every time the user scrolls. Fortunately, javascript provides an onscroll event that is pretty well supported. (Alternate strategies have included running a check every few milli-seconds, but that just doesn't feel very elegant to me). I use an anonymous function to unobtrusively call the onscroll actions that I need.

window.onscroll = function () {
     // do something
}

Great! We've figured out part 1 of our 3 part puzzle. But now we need to determine how far the user has scrolled, and this requires a bit more cleverness.

To understand how I implemented this, you first need to see something about the design of this site. The header is 268 pixels tall, and the menu is 50 pixels. So that means there is a total of (268+50) 318 pixels of space between the top of the page, and the scrollbar.

Given that 318px, I wanted to be able to have the sidebar scroll only if the banner or menu was visible. In other words, only if the user hasn't scrolled more then 318 pixels down the page, so I need a reliable way to detect how far the user has scrolled.

Bummer, because I'm not aware of a "proper" javascript way to detect how far we are from the top of the page, but Microsoft implemented scrollTop as an extension some time ago, and Mozilla implemented it also. Evidently so did Opera and others since it seems to work for those as well (albeit, not as nicely in Opera, as it is a bit "choppy"). So to figure out how far you are down a page, this will work.

<script type="text/javascript">
alert (document.documentElement.scrollTop);
</script>

So now we've got parts 1 and 2 solved. We can capture scroll events, and read how far we are from the top. The final step is to change the style of the sidebar. By default it starts off positioned absolutely, which means it will freely scroll up and down the page. To change any CSS style using javascript, you can just manipulate the style property. So to "lock" an item into position so it doesn't scroll, we need to make it's position "fixed".

document.getElementById('side_bar').style.position = 'fixed';

And to put it back into the normal flow of the page:

document.getElementById('side_bar').style.position = 'absolute';

At this point its worth bringing up an ongoing problem with CSS fixed positioning. Until recently the most widtly used browser in the world didn't support it, leading to a rash of /* IE Sucks */ comments in style sheets everywhere.. With the introduction of IE 7, fixed positioning works nicely. Unfortunately, there is still a large userbase who will continue to use IE 6 and lower for many years now. For my site, I chose to exclude people using IE 6 and lower from seeing the "locking" side bar. If you visit with IE 6, the bar will just continue to scroll up and down as you'd expect any other content to scroll.

Here's the whole thing put together. I use a simple "if" statement to toggle whether the sidebar should be fixed or absolute.
edit: Thanks to Cliff for raising this in the comments, I'm also using the wonderful dollar function $() from prototype in this example to replace document.getElementById().

window.onscroll = function()
{
	if( window.XMLHttpRequest ) { // IE 6 doesn't implement position fixed nicely...
		if (document.documentElement.scrollTop > 318) {
			$('side_bar').style.position = 'fixed'; 
			$('side_bar').style.top = '0';
		} else {
			$('side_bar').style.position = 'absolute'; 
			$('side_bar').style.top = 'auto';
		}
	}
}

A few more notes on that. Because the behaviour in IE 6 and lower is (hmmmm.. how do I put this delicately...) awful, I need to make sure that only modern browsers try to run this. There have been a few suggestions on how to do this, including Abe Fetting's article "Detecting IE7+ in Javascript", but I ultimately settled on just looking for the XMLHttpRequest object for its sheer simplicity. Before version 7, Internet Explorer implemented this as an ActiveX "thingy" (I think that's the technical term), but as of 7 it is a native browser object, so I believe this to be reasonably reliable. Also, it prevents that crappy userAgent testing from the days of Netscape 3.

So there you go! It works excellently in Firefox/Mozilla/Camino, and IE 7. It works well in Opera 8+, and won't work in IE 6 or lower. I've had a report of it working well in Safari, (I've since switched to a Mac and I can indeed confirm it works ;)) but I'd love to hear from a few more people on that front (c'mon, I know you're out there! All 6.83% of you!)

browser usage on DerekAllard.com.  6.83% use Safari

Now, I must say something about usability/accessibility with this. If someone comes on a very small resolution or small viewport size (less then 499px) then parts of the sidebar do get chopped off and aren't accessible to the user without disabling javascript, or making the viewport larger (not always an option). I know this. It was a decision I willingly made. In regularly monitoring my screen resolution statistics on this site, I've only ever seen 1 viewer, ever, who used a resolution 800x600 or smaller. Also, this blog tends to speak to a pretty tech-saavy crowd, so I'm not that concerned about it. Besides, modifying the script to first check viewport height would be trivially easy.

Comments

Matthew Pennell wrote on

It’s funny - I had *exactly* the same functionality on my comment form when I was building the latest version of my site. It was all working perfectly (including in IE6), then somehow I broke it when adding some other stuff, and I never got round to fixing it.

PS: That JS alert is still preventing me submitting a comment. :(

Lewis Wright wrote on

If you’re going to use Javascript, why not go the whole hog and use Javascript to position the sidebar? You get MSIE < 7 support then. It’s not very hard to do, just some simple maths.

I also get the Javascript alert. If you see this comment, then I had to disable Javascript to comment.

Derek wrote on

Thank you both.  Regarding IE 6, I had it working quite nicely, in earlier implementations of the site, and I completely agree with you both that it’s a trivial matter.  For me it was a personal choice.  I’m tired of fixing web pages to work around the inadequacies of IE.  The page is still usable for those surfing in IE, its just that the user experience is better for those who browse happy.
As an aside, an alternate to using javascript to position the sidebar in IE 6 is to use Microsoft proprietary expressions in the CSS. The expression statement works nicely, and combined with conditional comments could make it work no problem.
Apologies also for the poorly implemented validation on the comment form.  I’ve taken it off… (I wonder how many comments didn’t make it because of that oversight?)

Jonathan Snook wrote on

Fixed comments and sidebar? What kind of loser would ever do that? Oh wait…

(well done! ;) )

Derek wrote on

Jonathan Snook, the grand poo-ba of sticky-comments.  Welcome!  For what its worth, they aren’t sticky in IE 6.  I’m sure that is relieving for you ;)

Cliff wrote on

Just a quick thing. You should probably let ppl know that you’re using prototype in your final code and have replaced document.getElementById with the $

Dean Edwards wrote on

Here is a hack to get postition:fixed working in IE5/6:

http://dev.jdenny.co.uk/css/ie_fixed.html

Additional info:

http://dean.edwards.name/IE7/notes/#fixed

Johan Sundström wrote on

You want to use (window.)pageYOffset. In Safari too.

Dave L wrote on

Following up on Johan’s comment, I whipped up a demo page (local only, sorry) and changed

if (document.documentElement.scrollTop > 318)

to

if (document.documentElement.scrollTop > 318 || self.pageYOffset > 318)

and the same for the "else" condition, which worked in Safari 2.0.4, Firefox 2 (PC/Mac), Opera 9 (PC/Mac), and IE 7.

But in the "else" condition, I had to change " style.top = ‘auto’ " to " style.top = ‘318px’ ".

Hope this helps.

 

Derek wrote on

Wow!  Johan and Dave… you guys are great.  Thanks all for your help gentlemen.  I’ve instituted the safari fix.

@Dean: I sincerely appreciate your time, but I think I’ve decided that IE 6 is more trouble then its worth… so I’m going to leave it unscrolling for now.

Johan Sundström wrote on

The stickiness would be even better off turned off when the vertical size of the sticky pane (node.offsetHeight) exceeds the inner height of the window (self.innerHeight || document.documentElement.­clientHeight).

safarista wrote on

Works for me. With Safari. Hmm.

Michael Bernstein wrote on

Is there any way to get this working with an em based measure from the top of the page instead of pixels?