Tutorial

Build a Custom JavaScript Scrollspy Navigation

Draft updated on Invalid Date
author

Luis Manuel

Build a Custom JavaScript Scrollspy Navigation

This tutorial is out of date and no longer maintained.

Introduction

The content of an HTML document can be very long and difficult to access only through the scroll. Because of this arduous task, developers often use internal links (page jumps) as an alternative mode of transport around the page. This useful technique has been improved with the help of Javascript to offer a better experience, primarily by offering soft jumps and then introducing the so-called Scrollspy scripts.

A Scrollspy is used to automatically update links in a navigation list based on scroll position.

Through this tutorial, we’ll be building a custom Scrollspy component. See exactly what we are going to build below:

Custom Scrollspy

Also, you can take a look at the working DEMO.

To accomplish this custom Scrollspy we will be using:

  • Gumshoe: Simple, framework-agnostic scrollspy script.
  • Smooth Scroll: Lightweight script to animate scrolling to anchor links.
  • Anime.js: Flexible yet lightweight Javascript animation library.

Along with the tutorial, we’ll be explaining some features we use of these libraries, but it’s a good idea to check the Github repositories, for basic understanding.

Markup

Let’s start with the HTML structure we’ll be using, describing the key elements in the comments:

<section>
    <!-- Fixed header -->
    <!-- The [data-gumshoe-header] attribute tell Gumshoe that automatically offset it's calculations based on the header's height -->
    <!-- The [data-scroll-header] attribute do the same thing but for Smooth Scroll calculations -->
    <header class="page-header" data-gumshoe-header data-scroll-header>
        <div class="page-nav">
            <!-- Nav and links -->
            <!-- The [data-gumshoe] attribute indicates the navigation list that Gumshoe should watch -->
            <nav data-gumshoe>
                <!-- Turn anchor links into Smooth Scroll links by adding the [data-scroll] data attribute -->
                <a data-scroll href="#eenie">Eenie</a>
                <a data-scroll href="#meanie">Meanie</a>
                <a data-scroll href="#minnie">Minnie</a>
                <a data-scroll href="#moe">Moe</a>
            </nav>
            <!-- Arrows -->
            <a class="nav-arrow nav-arrow-left"><svg class="icon"><use xlink:href="#arrow-up"/></svg></a>
            <a class="nav-arrow nav-arrow-right"><svg class="icon"><use xlink:href="#arrow-down"/></svg></a>
        </div>
    </header>
    <!-- Page content -->
    <main class="page-content">
        <section>
            <h2 id="eenie"><a data-scroll href="#eenie">Eenie</a></h2>
            <p>Lorem ipsum dolor sit amet, has dico eligendi ut.</p>
            <!-- MORE CONTENT HERE -->
        </section>
    </main>
</section>

Adding style

With the HTML ready, we are all set to add some style. Let’s see the key style pieces commented briefly:

h2 {
  /* This is to solve the headbutting/padding issue. Read more: https://css-tricks.com/hash-tag-links-padding/ */
  /* 110px = 80px (fixed header) + 30px (additional margin) */
  &:before {
    display: block;
    content: " ";
    margin-top: -110px;
    height: 110px;
    visibility: hidden;
  }
}

/* Fixed header */
.page-header {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 80px; /* The height of fixed header */
  background-color: #2D353F;
  text-align: center;
  z-index: 2;
}

/* Content container */
.page-content {
  display: inline-block; /* This is for clearing purpose. */
  margin: 80px 50px 30px; /* Margin top = 80px because of fixed header */
}

/* Nav container */
.page-nav {
  display: inline-block;
  position: relative;
  margin-top: 20px;
  height: 40px; /* This is the same height of each link */
  width: 400px;
  max-width: 100%; /* Responsive behavior */
  overflow: hidden; /* Only current link visible */
  background-color: #427BAB;
}

/* Nav and links */
nav {
  position: relative;
  width: 100%;
  line-height: 40px;
  text-align: center;
  background-color: rgba(0, 0, 0, 0.05);

  a {
    display: block;
    font-size: 18px;
    color: #fff;
    outline: none;
  }
}

Adding functionality and animations

As we will be working closely with the DOM, we need to get all the elements we need first. Also, we will declare the additional variables we will be using.

// Init variables
var navOpen = false;
var pageNav = document.querySelector('.page-nav');
var navEl = document.querySelector('.page-nav nav');
var navLinks = document.querySelectorAll('.page-nav nav a');
var arrowLeft = document.querySelector('.nav-arrow-left');
var arrowRight = document.querySelector('.nav-arrow-right');
var navHeight = 40;
var activeIndex, activeDistance, activeItem, navAnimation, navItemsAnimation;

The following is a key part of the puzzle. This function translates the nav element to show only the selected link, using the activeIndex value.

// This translate the nav element to show the selected item
function translateNav(item) {
    // If animation is defined, pause it
    if (navItemsAnimation) navItemsAnimation.pause();
    // Animate the `translateY` of `nav` to show only the current link
    navItemsAnimation = anime({
        targets: navEl,
        translateY: (item ? -activeIndex * navHeight : 0) + 'px',
        easing: 'easeOutCubic',
        duration: 500
    });
    // Update link on arrows, and disable/enable accordingly if first or last link
    updateArrows();
}

Then, we need a way to open and close the nav. The open state should let us see all the links and allow us to select one of them directly. The close state is the default one, letting see only the selected link.

// Open the nav, showing all the links
function openNav() {
    // Updating states
    navOpen = !navOpen;
    pageNav.classList.add('nav-open');
    // Moving the nav just like first link is active
    translateNav();
    // Animate the `height` of the nav, letting see all the links
    navAnimation = anime({
        targets: pageNav,
        height: navLinks.length * navHeight + 'px',
        easing: 'easeOutCubic',
        duration: 500
    });
}

// Close the nav, showing only the selected link
function closeNav() {
    // Updating states
    navOpen = !navOpen;
    pageNav.classList.remove('nav-open');
    // Moving the nav showing only the active link
    translateNav(activeItem);
    // Animate the `height` of the nav, letting see just the active link
    navAnimation = anime({
        targets: pageNav,
        height: navHeight + 'px',
        easing: 'easeOutCubic',
        duration: 500
    });
}

Now let’s see how we handle the events. We need handlers to open or close the nav accordingly.

// Init click events for each nav link
for (var i = 0; i < navLinks.length; i++) {
    navLinks[i].addEventListener('click', function (e) {
        if (navOpen) {
            // Just close the `nav`
            closeNav();
        } else {
            // Prevent scrolling to the active link and instead open the `nav`
            e.preventDefault();
            e.stopPropagation();
            openNav();
        }
    });
}

// Detect click outside, and close the `nav`
// From: http://stackoverflow.com/a/28432139/4908989
document.addEventListener('click', function (e) {
    if (navOpen) {
        var isClickInside = pageNav.contains(e.target);
        if (!isClickInside) {
            closeNav();
        }
    }
});

We are ready to let Gumshoe and Smooth Scroll do the magic. See how we are initializing them:

// Init Smooth Scroll
smoothScroll.init({
    // This `offset` is the `height` of fixed header
    offset: -80
});

// Init Gumshoe
gumshoe.init({
    // The callback is triggered after setting the active link, to show it as active in the `nav`
    callback: function (nav) {
        // Check if active link has changed
        if (activeDistance !== nav.distance) {
            // Update states
            activeDistance = nav.distance;
            activeItem = nav.nav;
            activeIndex = getIndex(activeItem);
            // Translate `nav` to show the active link, or close it
            if (navOpen) {
                closeNav();
            } else {
                translateNav(activeItem);
            }
        }
    }
});

Conclusion

And we are done! You can see it working here.

For the sake of clarity, we have commented only the most important parts of the code. But you can get it all from this GitHub repo.

We really hope you have enjoyed it and found it useful!

Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.

Learn more about our products

About the authors
Default avatar
Luis Manuel

author

Still looking for an answer?

Ask a questionSearch for more help

Was this helpful?
 
Leave a comment


This textbox defaults to using Markdown to format your answer.

You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!

Try DigitalOcean for free

Click below to sign up and get $200 of credit to try our products over 60 days!

Sign up

Join the Tech Talk
Success! Thank you! Please check your email for further details.

Please complete your information!

Become a contributor for community

Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.

DigitalOcean Documentation

Full documentation for every DigitalOcean product.

Resources for startups and SMBs

The Wave has everything you need to know about building a business, from raising funding to marketing your product.

Get our newsletter

Stay up to date by signing up for DigitalOcean’s Infrastructure as a Newsletter.

New accounts only. By submitting your email you agree to our Privacy Policy

The developer cloud

Scale up as you grow — whether you're running one virtual machine or ten thousand.

Get started for free

Sign up and get $200 in credit for your first 60 days with DigitalOcean.*

*This promotional offer applies to new accounts only.