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:
- The header's top offset, or distant between the top of the header and the very top of the document
- 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 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.