Change Active State of Links in Sticky Navigation on Scroll

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:

<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>

And this is the 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 {}

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:

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');
    }
  });
});

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 of height() so that we can take in account of the entire height, including padding as well as borders.
  • Notice the deduction of nav_height to calculate top. 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().top and 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.

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;
});

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.

Author: Rishabh

Rishabh is a full stack web and mobile developer from India. Follow me on Twitter.

30 thoughts on “Change Active State of Links in Sticky Navigation on Scroll”

  1. Hello,
    I’m doing an example page base in you “Change Active State of Links in Sticky Navigation on Scroll”.
    I can change the section names and add new sections. The problem that I have is that the change between the active states is replacing the section and no showing the movement during the change. I think It could be a problem related to the “jquery.min.js”. I tried to make a local copy and update the reference, but it didn’t change. Can you send me a complete page? Because a copy the diferenct sections as it was in the example and it doesn’t work to me. Thanks in advantage.
    Martín Yasse (from Argentina)

  2. Hi Rishabh,

    Thank you so very much for your quick response to my request yesterday.

    After writing to you, spent whole day to debug the issue, and only today morning realised that I have <div class="nav"></div> in my navigation, which I change to <nav> tags.
    Everything is working, along with the- return false; and other js functions Your code is perfect :))

    I have a request, would you be willing to review and provide suggestion for my web design portfolio that I am working on, at your convenience.

    Greatly appreciate for sharing knowledge. Best of wishes & blessings to you.

    Warm Regards,
    Neha

  3. Hello
    I’ve paste the html, the css and the javascript codes in dreamweaver, linked the 3 files but the result is not the same that on your website.
    When I test my website in a browser, the fluid navigation doesn’t work, the little line under the menu doesn’t appear and when i navigate on each section the menu is align on the top of the word (first, second,…) and not on the separation between the section’s colors.
    Can you help me ?
    Thank you

    1. Make sure you properly defined the html5 doctype with opening and closing html, head and body tags. The HTML code in this article needs to go into your body tags and then have a script tag at the end of your body that’ll contain the JS code while a style tag in your head section with the CSS code. You can also have external stylesheet and script files.

      Feel free to upload your website to a server where I can test and debug as well to troubleshoot your problem.

        1. how do you link the .js script inside the html?

          how can I load the jquery inside the html? the .js file is inside the same folder where the html is.

          ty!

  4. Hi there, thanks for the tutorial!

    I can’t figure out how to the active state on scroll to work. Is there a way I can download a working version?

    Thanks,
    Paul

  5. Thanks for good tip! but…
    ie11 can’t detect height of sections, when changing section’s “height:auto” in css.
    How can i fix it?

  6. Thx for share, I have a situation.

    It works great but in my active class I have font color green, cause of that all texts are bein green while I scroll how can I fix it I don’t want my section will get the active class. I need only my nav childiren I mean li or a tags will be active how to fix the JS code ? Thx.

  7. I’m confused, I gave it a ” ” inside my index.html, then added the “” for the location of my .js file. may I ask what I’m missing here?
    I’m a noob and learning hehehe..

    By the way, what is the “class=”cssdeck” for? Will greatly appreciate the reply, thanks! lol

  8. Great tutorial! Only thing is when scrolling my content goes over the nav and hides it. Also, it doesn’t show the underline when active. Any suggestions on it? Thank you.

  9. Hi man, thanks for the great tutorial, but it seems is not working for me, neither in the live demo. I coppied and pasted the exact same code, and still no response from the smooth scrolling, can you help me with that please?

Leave a Reply

Your email address will not be published. Required fields are marked *


*