Several days ago I finally had some time to update the UI of this blog application and implement our new look-and-feel. Although it wasn't supposed to be a major overhaul - just a change of graphics, styles and a bit of layout tweaking - I also wanted to make the left side navigation to stay visible when the user scrolls the page or resizes it. You know, like those fancy "social media" vertical bars. They are almost mandatory these days :)
Because I'm unbelievably lazy and refuse to work unless it's absolutely necessary, I went hunting for a script or component that already provides that functionality. To my surprise, I couldn't find the one that works the way I needed it to. Therefore, I had to create our own and wanted to share with you the result of that effort, namely, the left-side navigation bar on this site. You can also test the
live demo HTML page which uses a much cleaner and "environment-free" version of the same script.
Although not necessary, the demo page uses jQuery for the DOM manipulation. The actual nav bar on this page, however, employs pure JavaScript (some functions are buried inside of our own framework.) It would be useful to check both places if you are new to client scripting.
To start, let's define the requirements:
1. If the height of the main content of the page is higher than the height of the navigation bar itself, the bar must remain visible when the user scrolls the page up or down.
2. The footer of the page should "push" the bar up, if the space under the footer is big enough or the height of the entire window is small enough to allow the user to scroll that much. (I didn't know how to explain this one in a more understandable fashion. Just open the
demo page and scroll all the way down - you'll see what I mean.)
3. If the height of the content is smaller or equal to the height of the nav bar, the bar must move together with the rest of the page when the user scrolls it, as if the "always visible" functionality wasn't implemented.
The HTML layout used on the demo page is dead simple and resembles the layout of a typical website. There are two DIVs, the "container" and the "footer". The container has the "header" (the blue DIV), the "navigation" (the gray DIV) and the "content" (the yellow DIV.) Navigation has the "bar" (the green DIV.) We are going to make that bar DIV stay visible on the page scroll. We need both the navigation and the bar DIVs if we want to fulfill the requirement # 2. (There is a link to a .zip archive at the bottom of this post that contains HTML used by the demo page.)
The navigation is floated to the left, the content is floated to the right. The bar's position is "fixed". Its fixed positioning is very important. For instance, if you remove all scripting from the demo page, open it in your browser and scroll the page up and down, you'll see that the bar stays in its original vertical position even when you resize the page. This is due to the fixed positioning. Although close, it's not good enough yet to consider our job to be done. We need some scripting to make the bar to behave according to our requirements.
I'm going to declare references to all frequently accessed
static elements and values as global variables. This will significantly lower the amount of client's CPU work needed to handle window's onscroll event, which we are going to subscribe to later. We need to declare variables for the navigation DIV, the bar DIV and heights of the container, the header and the bar.
<script type="text/javascript">
var
navigation, bar,
containerHeight, barHeight, headerHeight;
</script>
Then we need to set our variables to reference our DIVs and calculated heights. We do this when the page is done loading by either subscribing to body's onload event or by using jQuery's shortcut that does that for us:
$(function ()
{
bar = $("#bar"), navigation = $("#navigation"),
barHeight = bar.height(),
headerHeight = $("#header").height();
});
Notice that we didn't calculate the containerHeight yet. Because of the bar's fixed position, the navigation DIV will have the same height as the content DIV. This will mess up our layout if the bar is taller than the content. To deal with this, first we need to get the height of the bar, and then set the minimum height of the navigation DIV to the bar's height. And only then we can calculate the proper height of the container. At the end of all this we need to subscribe to window's onscroll event, passing it a function named "scrolled" as its handler. Here is what we got so far:
<script type="text/javascript">
var
navigation, bar,
containerHeight, barHeight, headerHeight;
// Init everything when the page loads
$(function ()
{
bar = $("#bar"), navigation = $("#navigation"),
barHeight = bar.height(),
headerHeight = $("#header").height();
// Because the position of the bar is fixed, the min height of
// its parent (the navigation div) must be set manually
navigation.css("minHeight", barHeight + "px");
// Calculate and set in advance the height of the
// container AFTER setting the height of the navigation
containerHeight = $("#container").height();
// Subscribe to the window.onscroll event
$(window).scroll(scrolled);
});
// The window.onscroll event handler
function scrolled()
{
// More code will go here...
};
</script>
What's left is the event handler. One of the most important values in client scroll calculations is the current scrollTop. That's the first thing that we need to calculate in our handler. For the reference: scrollTop can be expressed as the length in pixels between the top of the window and the top of scroll bar, as I beautifully illustrated below utilizing my yet-to-be-discovered drawing talent:
Strictly speaking, the JavaScript specification defines it as the number of pixels that are hidden from view above the scrollable area. But I find that definition to be more difficult to visualize. In the past, it was quite a challenge to get the proper value of the scroll top. Modern client frameworks, such as jQuery, make that process a snap:
function scrolled()
{
var scrollTop = $(window).scrollTop();
};
It cannot get any easier than this. So, at this point we have everything we need. Now we only have to set the top position of the bar DIV every time the user moves the scroll bar. According to our requirements, we need to handle two cases:
We need to monitor if the remaining visible height of the container is still larger than the height of the bar. This can be expressed as the following condition:
if (containerHeight - scrollTop > barHeight)
Then, while the above condition is True, we need to check for two more conditions. The bar must sit in its original place and move together with the rest of the page while the header is fully or partially visible. And it must freeze at the top of the page and ignore scrolling when the header gets completely hidden. Under these two conditions, the top position of the bar can be calculated like this:
scrollTop < headerHeight ? (navigation.offset().top - scrollTop) : 0
If the first condition is False, the top of the bar should be set to the following value (this also takes care of the requirement # 2:)
containerHeight - scrollTop - barHeight
So, after figuring out all those details, our handler should look like this:
function scrolled()
{
var scrollTop = $(window).scrollTop();
if (containerHeight - scrollTop > barHeight)
bar.css("top", (scrollTop < headerHeight ? (navigation.offset().top - scrollTop) : 0) + "px");
else bar.css("top", containerHeight - scrollTop - barHeight + "px");
};
And that's pretty much it. Again, the .zip file containing the entire code is available at the bottom of this post.
A couple of important notes, though. The demo page uses very "clean" elements that don't have margins, padding and dynamic content. In real life, you need to consider all styles that may alter your layout, such as body margins and things of that nature. For instance, this page appends all comments to its main container after the DOM is loaded and all static elements are initialized. Remember that our calculation of the container's height is done on page load? That wouldn't work in this application because the true height of the main container is not ready on page load yet. I had to make our Comment class throw a custom event when it's done appending its data. The bar script handles that event and sets the height of the container at that moment. Things like that can easily ruin a day or two, so plan your stuff carefully :)
Happy programming!