How to build a double sticky nav

By Jack,

April 2019

Web Dev
Scroll past great pop culture references at your own peril.

A sticky navigation in plain English is a navigation bar on a website thatsticks to the device window once the user starts to scroll.

Traditionally sticky navigations stick to the top of the device window (however this isn't always the case) and the navigation bar follows the user down the website as they browse the page.

Let's see the design

The main takeaways from the design are:

  • You have a traditional header (primary navigation) at the top of the page that contains links to other pages across the website.
  • A secondary navigation contains links that once clicked, scrolls the user to relevant content sections for the page they're on.
  • Plot twist - the secondary navigation needs to stick below the primary navigation whilst the user scrolls down the page
AFR-navigation
Header design
Navigation different states
The various states the navigation bar needed to be in throughout the website

What are we going to do?

The idea

When the secondary navigation comes into contact with the primary navigation, we're going to add a class to attach the secondary navigation to the bottom of the primary navigation. When the page scrolls back up, we'll remove the class, leaving the navigation back where it was originally.

Sounds pretty straight forward right?

Note: We're using BEM for our class names, if you want to know more check out the blog I wrote about BEM and how it makes naming classes a doddle.

The markup

The primary navigation will allow the user to get around the website, you'll then need a div that houses your hero content and this is where you would normally include your product offers, photos of your dog, whatever the design entails.

Then for the secondary navigation (the links that allow the user to click and scroll to the content on the page in our case) is just another standard navigation.

<div class="sticky-wrap">
  <nav class="nav nav--primary">
    <ul class="nav__items">
      // However many nav items you need
      <li class="nav__item">
        <a href="/">
          Primary 1
        </a>
      </li>
    </ul>
  </nav>

  <div class="hero hero__content">
    Amazing hero content
  </div>

  <nav class="nav nav--secondary">
    <ul class="nav__items nav__items--secondary">
      // However many sub items you need
      <li class="nav__sub-item">
        <a href="/" class="link">Secondary 1</a>
      </li>
    </ul>
  </nav>
</div>

The nav-fixed class

I've not included the majority of the navigation styles but they're in the codepen below if you want them.

The most important class is this .nav-fixed class as it's added by the Javascript depending on the scroll position of the secondary navigation.

.nav-fixed {
  &--secondary {
    position: fixed;
    bottom: auto;
  }
}

Show me the Javascript

Have you ever tried writing out Javascript logic? It's surprisingly tricky...

This is where the fun begins

Firstly we need to get our HTML elements from the DOM into our JavaScript file, and then get the heights & positions of the elements that are going to be nesting underneath each other.

const navPrimary = document.querySelector(".nav--primary");
const navSecondary = document.querySelector(".nav--secondary");
const stickyWrap = document.querySelector(".sticky-wrap");
const contentWrap = document.querySelector('.content-wrap');

let navPrimaryHeight = navPrimary.getBoundingClientRect().height;
let navSecondaryHeight = navSecondary.getBoundingClientRect().height;
let navSecondaryTop = navSecondary.getBoundingClientRect().top;

When does this cross that?

First we need to get the bottom position of the .sticky-wrap and then take away the height of the secondary nav, so that we have the top position of the secondary navigation element on the page.

We then take into account the users scroll position on the page to ensure we've always got the correct position.

let navSecondaryOrigPos = stickyWrap.getBoundingClientRect().bottom - navSecondaryHeight + window.pageYOffset;

Is the position of the secondary navigation bar above or below the .sticky-wrap's bottom?

We check to see if height of the primary navigation is equal to the top position of the secondary nav. If they are, it means the secondary nav is underneath the primary navigation and we add the class to make it fixed.

If the original position of the secondary navigation is greater than the pixels scrolled, it means the user hasn't scrolled enough to make the navigation sticky so we remove the class.

navSecondaryTop = navSecondary.getBoundingClientRect().top;

if (navSecondaryTop <= navPrimaryHeight) {
  navSecondary.classList.add("nav-fixed--secondary");
}

if (window.scrollY + navPrimaryHeight < navSecondaryOrigPos) {
  navSecondary.classList.remove("nav-fixed--secondary");
}

Sticky navs make you jump, jump

The sum of both navigations' heights needs to be added to the .content-wrap beneath the hero section to ensure the content doesn't jump when the second navigation becomes fixed (cue Kris Kross). We add this value to the position: top of the .content-wrap

When they scroll back up, we remove the adjusted positioning to the .content-wrap.

Let's wrap all of this logic in a function called stickyNavInit so that we can call it when the user scrolls.

function stickyNavInit() {
  navSecondaryTop = navSecondary.getBoundingClientRect().top;

  if (navSecondaryTop <= navPrimaryHeight) {

    navSecondary.classList.add("nav-fixed--secondary");
    
    navSecondary.style.top = navPrimaryHeight + "px";

    contentWrap.style.paddingTop = (navSecondaryTop + navPrimaryHeight) + "px";   
    
  }
  
  if (window.scrollY + navPrimaryHeight < navSecondaryOrigPos) {
    navSecondary.classList.remove("nav-fixed--secondary");
  
    navSecondary.removeAttribute("style");
    
    contentWrap.removeAttribute("style");
  }
}

window.addEventListener("scroll", stickyNavInit);

I'll try throttling, that's a good trick

Firing an event listener for each scroll event as the user scrolls down the website is going to reduce performance. This is where throttling comes into play.

To put it simply; throttling reduces the number of times a function fires, which puts less strain on the browser.

Some Googling can help you find a throttling function. Credit and further reading on the throttler function we're using below.

function throttled(delay, fn) {
  let lastCall = 0;
  return function (...args) {
    const now = (new Date).getTime();
    if (now - lastCall < delay) {
      return;
    }
    lastCall = now;
    return fn(...args);
  }
}

so the last line of our code now looks like this:

throttled(200, window.addEventListener("scroll", stickyNavInit));

It's working! It's working!

Look at all of that double sticky nav goodness! We've got two navigation bars that now follow the user around on desktop.

If you're creating a website that is desktop focused by the users (they still exist, trust me) then this could be a handy UX decision you could include.

The Codepen is best viewed on a desktop.

Thoughts? Questions? Comments? Concerns?

Drop us a line on Twitter.

I also want to credit the Wes Bos JavaScript 30 course that pointed me in the right direction when getting started with the implementation of this sticky header.