Categories:

Creating a sticky header bar using jQuery and CSS

Updated: July 28th, 15'

A hot trend in web design these days is the use of sticky headers, where the top portion of a page containing essential elements such as the menu bar once scrolled past becomes fixed on the page, continuing to remain visible. The following is a example of a sticky header. As simple as the effect looks, implementing a well optimized, reliable sticky header is more involved than meets the eye. In this tutorial, we'll tackle the pitfalls and see how to create the ideal sticky header using jQuery and CSS:

Sticky header example we'll be creating

Preparation- a sample layout

First, lets erect a basic layout we'll be using to eventually make the header sticky in. It's a simple page with a top logo section, a header section, and finally, a main content section:

The CSS:

body{
	margin: 0;
	padding: 0;
}

body *{
	-webkit-box-sizing: border-box;
	-moz-box-sizing: border-box;
	box-sizing: border-box;
}

div#header{
	width: 100%;
	height: 100px;
	background: lightblue;
	margin: 0;
	padding: 5px;
}

div#contentarea{
	padding: 10px;
}

The HTML:

<div id="logo">
<a href="http://www.javascriptkit.com"><img src="jksitelogo.gif" /></a>
</div>

<div id="header">
<p>Some header content</p>
</div>

<div id="contentarea">
<b>Main Content Start here</b>
more text here
more text here
</div> 

See the Pen Sticky Header page- preparation by georgec (@georgec)

As you can see, all of the sections are simply positioned statically and sequentially. What we'll see next is how to target one of the sections- in our case the header- and make it fixed in position whenever the user starts to scroll past the top of that section.

Making the header sticky, or conditionally fixed in position

The key to making an element fixed is well, by using CSS's "fixed" property. What we want for a sticky element however is one that's only conditionally fixed, by selectively adding the CSS property to our element when those conditions are met, and removing it when not. In the case of a sticky header, the specific condition to examine is whether the user has scrolled past the top of the header (so part of it is obscured). To do this, we need to determine two things:

  1. The header's top offset, or distant between the top of the header and the very top of the document
  2. How much the user has currently scrolled vertically relative to the top of the document

The header's top offset value can be obtained in jQuery using the offset() method on the element:

targetoffsetTop = $('#header').offset().top // get distance between top of header element and top of document

To determine how much the user has scrolled the page relative to the top of the document, we call jQuery's scrollTop() method on the document itself:

var scrollTop = $(document).scrollTop()

Whenever the value of 2) exceeds the value of 1), that's when we want to fix the header on the page; during the other times, the header should be left in its unfixed state.

With the basic logic behind us, lets introduce the code now that makes the header section sticky, and break it down afterwards to explain how it works and the benefits of this approach:

Additional CSS:

body.sticky div#header{
	position: fixed;
	top: 0;
	left: 0;
	box-shadow: 0 5px 10px rgba(0,0,0,0.3);
}

body.sticky div#contentarea{
	margin-top: 100px; /* shift contentarea downwards by height of the header so it's fully in view when the header is fixed */
}

The sticky JavaScript:

window.requestAnimationFrame = window.requestAnimationFrame
	|| window.mozRequestAnimationFrame
	|| window.webkitRequestAnimationFrame
	|| window.msRequestAnimationFrame
	|| function(f){return setTimeout(f, 1000/60)}


;(function($){ // enclose everything in a immediately invoked function to make all variables and functions local

	var $body,
	$target,
	targetoffsetTop,
	resizetimer,
	stickyclass= 'sticky' //class to add to BODY when header should be sticky
	
	function updateCoords(){
		targetoffsetTop = $target.offset().top
	}
	
	function makesticky(){
		var scrollTop = $(document).scrollTop()
		if (scrollTop >= targetoffsetTop){
			if (!$body.hasClass(stickyclass)){
				$body.addClass(stickyclass)
			}
		}
		else{
			if ($body.hasClass(stickyclass)){
				$body.removeClass(stickyclass)
			}
		}
	}
	
	$(window).on('load', function(){
		$body = $(document.body)
		$target = $('#header')
		updateCoords()
		$(window).on('scroll', function(){
			requestAnimationFrame(makesticky)
		})
		$(window).on('resize', function(){
			clearTimeout(resizetimer)
			resizetimer = setTimeout(function(){
				$body.removeClass(stickyclass)
				updateCoords()
				makesticky()
			}, 50)
		})
	})

})(jQuery)

-The end result

The above code is all that's needed to add spider man powers to our header. Lets turn our attention first to function makesticky(), which is what, along with the additional CSS, actually sticks and unsticks the header based on the position of the user's scrollbar:

function makesticky(){
	var scrollTop = $(document).scrollTop() // how much user has scrolled
	if (scrollTop >= targetoffsetTop){
		if (!$body.hasClass(stickyclass)){
			$body.addClass(stickyclass)
		}
	}
	else{
		if ($body.hasClass(stickyclass)){
			$body.removeClass(stickyclass)
		}
	}
}

The function when called gets the most current value of the document' scroll top and compares that with the header's offset top value. If the former is larger, it adds a CSS class of "sticky" to the BODY, while if not removes this class. Then to actually make our header sticky, it hands over that task to CSS to target and style the header with a "fixed" position when the "sticky" class is present:

body.sticky div#header{
	position: fixed;
	top: 0;
	left: 0;
	box-shadow: 0 5px 10px rgba(0,0,0,0.3);
}

body.sticky div#contentarea{
	margin-top: 100px; /* shift contentarea downwards by height of the header so it's fully visible once header is fixed */
}

By using JavaScript to only add/remove a CSS class that indicates whether the condition has been met, and delegating the actual styling of the header in the two different states to CSS, we take advantage of the simplicity of pure CSS in defining the desired styles for the header. Notice how makesticky() checks the presence or absence of the "sticky" class each time it's called before adding or removing that class, respectively. This optimizes the code so it doesn't incessantly add/remove the "sticky" class to the BODY element, but only once each time the threshold for a change in the condition has been satisfied.

The makesticky() function is called whenever the window is scrolled or resized. We'll talk more about the later in a bit, but to realize the former, we attach a "scroll" event to the window object in jQuery and call the desired code to run inside it:

$(window).on('scroll', function(){
	requestAnimationFrame(makesticky)
})

Running code inside window's scroll event can be extremely expensive, potentially firing many times per second depending on how fast the user scrolls. It's here we apply another optimization technique, using window's requestAnimationFrame() method to call the makesticky() function instead of directly. This method intelligently throttles the execution of the function passed into it based on when the screen is ready for the next repaint, regardless of how many times requestAnimationFrame() is called in succession (as determined by how fast or slow the user scrolls the window). For code that is run inside window's scroll event and that makes changes to the user's screen, it is usually best practice to funnel it through requestAnimationFrame().

Our makesticky() function is also run when the window resizes, as contents on the page may have shifted during resize, as flight attendants would say. We are specifically interested in any change in the header's original offset top value (the distance between the header and the top of the document). In today's word of responsive design where elements heights can also be fluid, this is quite likely to occur. This is why when the window is resized, we have the following to get the most current offset top value of the header, and reassess whether the header should remain sticky or not:

$(window).on('resize', function(){
	clearTimeout(resizetimer)
	resizetimer = setTimeout(function(){
		$body.removeClass(stickyclass)
		updateCoords()
		makesticky()
	}, 50)
})

Inside the highlighted code, the first line is of paramount importance. It temporarily reverts the header back to its original, "unfixed" state so the correct new offset top value of the header can be obtained afterwards. Trying to get this value while the header is fixed in position returns the distance the header has travelled from the top of the document to remain fixed on the page, which is not what we want. With the header momentarily unfixed, we call updateCoords() to first grab the latest top offset of the header, followed by makesticky() to determine how the header should be positioned (fixed or not) based on the new data.

Notice how the code we wish to run inside the resize event is called via a setTimeout() timer. This is another optimization technique that throttles the number of code invocations that occurs when the user resizes the window. The resize event of the window object in most browsers fires multiple times whenever the user resizes the window, instead of just once as the user goes from one screen size to another. This means any code inside this event is also fired numerous times each time, often unnecessarily. By using a setTimeout() timer to add a delay before executing our desired code, in combination with clearTimeout() that cancels the previously queued operation, the result is the desired code running just once at the end of the resize event. Use this technique to reduce a potential avalanche of calls to code inside window's resize event to a whimper.

Finally, our sticky header code is initialized when the document has fully loaded. It is at this point we get the header's offset top value to ensure it is accurate, after items such as images that proceed the header that may affect this value have loaded. If there are no such items on your page, you can quicken the initialization process by merely waiting for the DOM to load before making the header sticky, or combine the best of both worlds with something like the following:

	jQuery(function(){ // on DOM load
		$body = $(document.body)
		$target = $('#header')
		updateCoords()
		$(window).on('scroll', function(){
			requestAnimationFrame(makesticky)
		})
		$(window).on('resize', function(){
			clearTimeout(resizetimer)
			resizetimer = setTimeout(function(){
				$body.removeClass(stickyclass) // unstick header so we can get accurate header offset top value
				updateCoords()
				makesticky()
			}, 50)
		})
	})
	$(window).on('load', function(){
		$body.removeClass(stickyclass) // unstick header so we can get accurate header offset top value
		updateCoords() // get sticky header's offset top value again to ensure it's accurate
		makesticky()
	})

By running the script as soon as the DOM has loaded, but refresh the header's top offset value when the window has fully loaded, we can have our cake and eat it too! Notice how the code inside the window "load" event has now changed to include removing the "sticky" class from the BODY first before calling updateCoords()- this is for the same reason as why we do the this inside the window "resize" event as well, to get the correct top offset of the header while it's NOT "fixed" in position.

Conclusion

In this tutorial we learned how to make an element on the page conditionally fixed in position to create the popular sticky header effect. With the right considerations, especially code optimization taken into account, the result is a seamless, positive addition to your page's UI.