#Bootstrap

How to Add Smooth Scrolling with Offset to Bootstrap's Scrollspy

by Radu

I was working on a Bootstrap website today.

Since it’s a one-page website, it requires anchor links in the navigation menu, Scrollspy to make them active, smooth scrolling for a bit of fanciness, and offset if you have a fixed header/nav.

That made me remember how tricky and time-consuming it was for me – not being a JavaScript developer – to find a proper code that adds both smooth scrolling and offset for the anchor links.

So, in this guide, I want to show you how to do that, so you won’t have to lose time putting pieces together from different forums and blog posts.

Add Smooth Scrolling with Offset to Scrollspy in Bootstrap

I’ll give you the code, then break it down a little bit.

Note that this code isn’t exclusively for Scrollspy or Bootstrap. It works for anchor links in any type of project.

// SMOOTH SCROLLING PLUS OFFSET FOR FIXED NAV

$('a[href*="#"]')
// Remove links that don't actually link to anything
.not('[href="#"]')
.not('[href="#0"]')
.on('click', function(event) {   

    // Make sure this.hash has a value before overriding default behavior
    if (this.hash !== "") {

        // Store hash
        var hash = this.hash;

        // Using jQuery's animate() method to add smooth page scroll
        // The optional number (800) specifies the number of milliseconds it takes to scroll to the specified area
        // - 70 is the offset/top margin
        $('html, body').animate({
            scrollTop: $(hash).offset().top - 70
        }, 800, function() {

            // Add hash (#) to URL when done scrolling (default click behavior), without jumping to hash
            if (history.pushState) {
                history.pushState(null, null, hash); 
            } else {
                window.location.hash = hash;
            } 
        });
        return false;    
    } // End if
});

The code uses jQuery.

Since you already need jQuery (not for Bootstrap v5+) for the other Bootstrap features, there’s no need to run away from it for custom stuff.

Changing the offset

Change the number 70 from the following line with whatever offset you need.

scrollTop: $(hash).offset().top - 70

It’s in pixels and it handles the space between the navigation and the anchor.

Add the hash (#) in the URL when done scrolling

This allows the hash to appear/change in the URL only after the scrolling finishes and the section is displayed.

Notice how #services doesn’t change into #experience immediately.

It’s a small detail that will probably go unnoticed 99% of the time, but… I like it. 😃

Now, in some codes, you’ll only find something like this after setting the animation and offset:

window.location.hash = hash;

The offset will be broken by this. It will take effect, but then it will quickly jump to the anchor, which is expected behavior. Therefore, a part of the section will go under the fixed menu.

To fix that, you can use the history.pushState API, as you can see in the code.

This won’t affect the offset and will also prevent the hash to appear/change in the URL until the scrolling stops.

Alternatively, you can use history.replaceState.

if(history.replaceState) {
    history.replaceState(null, null, hash);
}

They are both supported by all major browsers.

What’s the difference between history.pushState and history.replaceState?

The way they handle the browser’s Back function.

Examples for the one-page website

With history.pushState, if a user comes from Google, then clicks on an anchor link, he/she will have to click Back three times to go back to the Google page.

With history.replaceState, the user only has to click Back once to go back to the Google page.

It depends on which behavior you prefer and think it’s best for your users.

You can find out more about history.pushState and history.replaceState here and here.

If you want to use the default click behavior

If you want the hash to appear/change in the URL right after the user clicks on the link, then remove or comment out these lines:

if(history.pushState) {
            history.pushState(null, null, hash);
        }
        else {
            window.location.hash = hash;
        }

return false;

Why isn’t Scrollspy’s data-offset handle the top offset?

Many people think (I did too) that the data-offset set in the <body> tag for Scrollspy will handle the space between the fixed navigation and anchor.

<body data-spy="scroll" data-target=".navbar" data-offset="70">

Nope, it doesn’t.

It handles the distance at which the link becomes active as the user scrolls toward the anchored section.

Notice how the Services link becomes active as I scroll (not click) toward the My Services section

Add Smooth Scrolling with Offset to Anchor Links From External Pages

The above code triggers on click, not on load. This means that it works for anchor links that are on that particular page.

If you have separate/external pages, too, such as Terms and Privacy, or if someone pastes your URL with the hash in the address bar, the smooth scrolling and offset won’t work.

So, you’ll have to add an additional code. Here’s a good one that I found:

// SCROLLING TO ANCHOR LINKS WITH OFFSET FROM EXTERNAL PAGES

$(window).on("load", function () {

    var urlHash = window.location.href.split("#")[1];

    if (urlHash &&  $('#' + urlHash).length) {
        $('html,body').animate({
            scrollTop: $('#' + urlHash).offset().top - 70
        }, 800);
    }
});

Now, if you click on an anchor link in the menu from an external page, it will have offset and smooth scroll effect.

That’s a Wrap

I hope this guide helped you to add smooth scrolling with offset to Bootstrap’s Scrollspy.

If some info is outdated or incorrect, or you have anything to add, say or ask, please contact me via Twitter or email.

About Radu

I've been working online, from home, for over 9 years. I learned a lot of different stuff related to websites. My main expertise is WordPress, but for some time, I started focusing more on web development.