Based on the design decisions made by you on your website (or webapp), you might probably end up having a sticky navigation, one that keeps on showing up in the view port as you scroll up or down. In such a situation, it’s a common desire to have the links in the navigation bar be scrollbar-aware and change their active state as the user scrolls through the page. Let’s see how to do this.
Setup
First of all, I’ll setup a basic webpage with a sticky navigation bar and different sections that the links would correspond to. Here’s the HTML code for it:
[html]
<nav>
<ul>
<li><a href="#1">First</a></li>
<li><a href="#2">Second</a></li>
<li><a href="#3">Third</a></li>
<li><a href="#4">Fourth</a></li>
<li><a href="#5">Fifth</a></li>
</ul>
</nav>
<div class="sections">
<section id="1"><h1>First</h1></section>
<section id="2"><h1>Second</h1></section>
<section id="3"><h1>Third</h1></section>
<section id="4"><h1>Fourth</h1></section>
<section id="5"><h1>Fifth</h1></section>
</div>
[/html]
And this is the CSS:
[css]
@import url(http://fonts.googleapis.com/css?family=Open+Sans);
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: ‘Open Sans’, sans-serif;
}
/* Navigation */
nav {
width: 100%;
height: 60px;
position: fixed;
top: 0;
background: #1ABC9C;
}
nav ul {
padding: 20px;
margin: 0 auto;
list-style: none;
text-align: center;
}
nav ul li {
display: inline-block;
margin: 0 10px;
}
nav ul li a {
padding: 10px 0;
color: #fff;
font-size: 1rem;
text-decoration: none;
font-weight: bold;
transition: all 0.2s ease;
}
nav ul li a:hover {
color: #34495E;
}
a.active {
border-bottom: 2px solid #ecf0f1;
}
/* Headings */
h1 {
font-size: 5rem;
color: #34495E;
}
/* Sections */
section {
width: 100%;
padding: 50px;
background: #fff;
border-bottom: 1px solid #ccc;
height: 500px;
text-align: center;
}
section:nth-child(even) {
background: #ecf0f1;
}
section:nth-child(odd) {
background: #bdc3c7;
}
.sections section:first-child {
margin-top: 60px;
}
section.active {}
[/css]
Here goes a live demo.
Change Active State on Scroll
We’ll first implement the functionality of turning on the active state on the correct link when scrolling. So if you’re scrolling across the third section, then the third link should attain a class through which it can be styled to appear active. This has to be done with JavaScript and we’ll be using jQuery as a dependancy:
[js]
var sections = $(‘section’)
, nav = $(‘nav’)
, nav_height = nav.outerHeight();
$(window).on(‘scroll’, function () {
var cur_pos = $(this).scrollTop();
sections.each(function() {
var top = $(this).offset().top – nav_height,
bottom = top + $(this).outerHeight();
if (cur_pos >= top && cur_pos <= bottom) {
nav.find(‘a’).removeClass(‘active’);
sections.removeClass(‘active’);
$(this).addClass(‘active’);
nav.find(‘a[href="#’+$(this).attr(‘id’)+’"]’).addClass(‘active’);
}
});
});
[/js]
Very simple code. All we do is bind a scroll event to the window object and then we check if the amount of current scrolling done (in pixels) is less than the top offset of top edge and more than the top offset of the bottom edge of any of the sections or not. If yes then add active class to that section as well as the corresponding link.
There are 2 interesting things to notice in the code though:
- Usage of
outerHeight()instead ofheight()so that we can take in account of the entire height, including padding as well as borders. - Notice the deduction of
nav_heightto calculatetop. This is done to make sure that the active state is only applied when the navigation bar stops overlapping the topmost part of a section. Try changing line 9 to just$(this).offset().topand then scroll down, you’ll know what I mean.
Change Active State on Click
When the links are clicked, the active states seems to switch just fine because the scroll event gets fired inherently. But if you notice intimately then you’ll see that the navigation bar overlaps the section after click. In order to fix this, we’ll have to attach a listener to the click event on all the navigation anchors that will scroll a little less to prevent overlapping.
[js]
nav.find(‘a’).on(‘click’, function () {
var $el = $(this)
, id = $el.attr(‘href’);
$(‘html, body’).animate({
scrollTop: $(id).offset().top – nav_height
}, 500);
return false;
});
[/js]
Now due to return false; the location.hash won’t get updated which means the different sections cannot have a unique hash-based permalink. On the contrary, if we remove that then we’ll notice a glitch due to the browser first navigating us to that section followed by the animation that lasts for 500ms. A workaround that I can think of in this situation is to set a custom hash. So something like location.hash = id + '-scroll'; and then parse that in listeners to hashchange and load events on window object (to scroll down).
One More Issue
If the last section’s height is less than the viewport then the scrolling amount (in pixels) will never fall between the top and bottom edges of that section. Hence, the last link won’t ever show up as active. In order to fix this either add more content (like a footer) beneath the last section or just check if the end of document has been reached or not to add the active state.
Final Demo
See the Pen Setting Active States on Sticky Navigations while Scrolling by Rishabh (@rishabhp) on CodePen.
Bonus
There’s this plugin called jQuery Waypoints that you can possibly use to shorten the code. It makes it easy to execute a piece of code in a callback whenever you scroll to a particular element.
For Twitter Bootstrap framework, they already have a scrollspy plugin to get this same job done!
Conclusion
Hope that helps! If you’ve any questions, let me know in the comments.