HTML & CSS
Article

Understanding Bootstrap’s Affix and ScrollSpy plugins

By George Martsoukos

One of the things you probably have noticed when you visit Bootstrap’s website is the right-side navigation that moves as you scroll through the page. This scrolling effect can be achieved combining the Affix and ScrollSpy jQuery plugins, both available as Bootstrap components.

Bootstrap's docs nav

In this article, I’ll show you how to add this cool feature to one of your own projects. As always, to better demonstrate these, I’ll use a demo project.

Let’s start by taking a look at the HTML structure of it.

The Basic Markup for Our Demo

For small screens we use a single column layout. For medium and large screens our layout will consist of two columns. On the one hand, we have the navigation, which occupies one quarter of a row’s width. On the other hand, we have the main content sections, which occupy the other three quarters of a row’s width. Each of these sections has an id attribute value that matches the href attribute value of the corresponding anchor (a) element.

In our example, we want the scrolling effect to occur only when the viewport exceeds 991px (i.e. medium and large screens). For this reason, we add the hidden-xs and hidden-sm helper classes to the navigation.

See the code below. Note that for brevity, we’re not including content that isn’t relevant to the technique.

<div class="col-md-3 scrollspy">
  <ul id="nav" class="nav hidden-xs hidden-sm" data-spy="affix">
    <li>
      <a href="#web-design">Web Design</a>
    </li>
    <li>
      <a href="#web-development">Web Development</a>
      <ul class="nav">
        <li>
          <a href="#ruby">
            <span class="fa fa-angle-double-right"></span>Ruby
          </a>
        </li>
        <li>
          <a href="#python">
            <span class="fa fa-angle-double-right"></span>Python
          </a>
        </li>

      </ul><!--end of sub navigation-->
    </li>    

  </ul><!-- end of main navigation -->
</div>
<div class="col-md-9">
  <section id="web-design">
  </section>
  <section id="web-development">
    <section id="ruby">
    </section>
    <section id="python">
    </section>

  </section>
</div>

Now that we understood the basic structure of our project, we can include the plugins to add the functionality.

Using Affix

The Affix plugin will help us “fix” the position of our navigation section, while allowing us to add vertical offsets to this fixed element, depending on where the user has scrolled.

To use the Affix plugin in our project, we have to specify the element that will receive the “affix” behavior. We can do this by adding the data-spy="affix" attribute/value to it. In our example, the desired element is the ul element.

The plugin toggles between three classes, described here:

  1. The affix-top class, which indicates that the element is in its top-most position.
  2. The affix class, which is added when the element starts to scroll off the screen, and which applies the position: fixed property to it.
  3. The affix-bottom class, which indicates the bottom offset of the element.

In brief, the plugin changes the element’s type of positioning as the user scrolls up and down the page, using the three classes to do so.

Note that we can optionally take advantage of all the classes in our implementation. The most important class is the affix one, which allows us to pin an element as the user scrolls down the page. However, depending on the structure of our project, we might want to use the affix-top and/or affix-bottom classes as well.

Let’s now see how we can include all three classes in our example.

First, we assign the affix-top class to the ul element. To do this, we can use either custom data-* attributes or JavaScript. Here’s the required jQuery/JavaScript:

$('#nav').affix({
    offset: {
        top: $('#nav').offset().top
    }
});

At this point, we have applied the element’s top position. This is around 390px. By default, its position is set to static. Even though Bootstrap mentions that no CSS positioning is required, I’ve set its position to relative. Here’s a screenshot with annotations to show what’s going on:

Our fixed navigation with Affix

The HTML of the ul element looks like this when the page first loads:

Generated HTML for ul

As you can see, when the user first starts to scroll, the ul element has the affix-top class. When the “scrolling” exceeds the element’s initial top position (around 390px), the ul receives the affix class and its position changes to fixed. Then we set its new top (try to change its value to see the difference) position and width properties. Here are the corresponding styles:

.affix {
  top: 20px;
  width: 213px;
}

@media (min-width: 1200px) {
  .affix {
    width: 263px;
  }         
}

Below is a screenshot that corresponds to this phase:

Next phase in scrolling with Affix

And the generated HTML:

Generated HTML when nav is fixed

As the user scrolls, the ul is pinned to the top of the page. Of course, this is a nice effect, but we eventually want to stop the pinning. We can do this by using the affix-bottom class. To cause the plugin to trigger this class, we have to specify the bottom offset of the target element. Again, this can be achieved by using either custom attributes or jQuery/JavaScript. Here’s how to do it with jQuery:

$('#nav').affix({
  offset: {
    bottom: ($('footer').outerHeight(true) + 
            $('.application').outerHeight(true)) + 
            40
  }
});

During this phase, we have to use CSS to specify the type of positioning for our element. For instance, we can apply position: absolute or position: relative to it. In our example, I’ve chosen the first option. We also set its width property. Here are the required styles:

.affix-bottom {
  position: absolute;
  width: 213px;
}

@media (min-width: 1200px) {
  .affix-bottom {
    width: 263px;
  }
}

At this final stage, our demo will look like this:

The final stage of scrolling with Affix

Note: This final screenshot is based on a typical desktop screen resolution.

And here is the generated HTML:

Final stage generated HTML

As shown above, based on the bottom offset that we defined with jQuery, the plugin applies a new top offset to the element.

Finally, it’s important to mention that in the case of this specific demo, we had to specify a bottom offset for the element. This is because there’s content after the education-related sections. For instance, if we didn’t specify an offset, our page would look something like this:

Result without a bottom offset

Now that we have our Affix plugin working, we can add the ScrollSpy functionality.

Using ScrollSpy

To add ScrollSpy to our project, we have to define the element we want to “spy” on during page scrolling. Usually this will be the body element. ScrollSpy also requires that we use a Bootstrap nav component.

First, we apply the data-spy="scroll" attribute/value to the “spied” body element. At this point, it’s worth mentioning that Bootstrap suggests setting the position of our “spied” element to relative.

Next, we identify the specific parts of that element we want to track by adding the data-target attribute to the element we’re spying on (again, in our case this is the body). The value of this attribute should match the id or the class of the parent of the nav component. So the body tag would look like this (notice the “.” included as part of the value of the data-* attribute):

<body data-spy="scroll" data-target=".scrollspy">

Similar to the Affix plugin, there’s also the option to activate the ScrollSpy plugin via JavaScript. In our example, we won’t use this option.

After activating the plugin, our HTML would look like this:

<body data-spy="scroll" data-target=".scrollspy">
<!-- content here... -->

  <div class="col-md-3 scrollspy">
    <ul id="nav" 
        class="nav hidden-xs hidden-sm" 
        data-spy="affix">
        <!-- nav items here... -->
    </ul>
  </div>

  <!-- content here... -->
</body>

And the only necessary CSS:

body {
  position: relative;
}

Notice that we “watch” the direct parent element of the ul and not the ul.

We can use our browser’s developer tools to understand what the plugin does. While the user is scrolling, the plugin “watches” the target element and adds the active class to any appropriate li child elements. We then style the active elements based on this class. Some of our li elements contain sub navigation elements, which we can also style to see a nice effect.

The screenshot below shows the generated HTML when the ScrollSpy plugin is in use:

Generated HTML with ScrollSpy

Below are the styles I’ve applied to the nav component in our demo. Notice the styles take into account that I’m using nested nav components.

.nav .active {
  font-weight: bold;
  background: #72bcd4;
}

.nav .nav {
  display: none;
}

.nav .active .nav {
  display: block;
}

.nav .nav a {
  font-weight: normal;
  font-size: .85em;
}

.nav .nav span {
  margin: 0 5px 0 2px;
}

.nav .nav .active a,
.nav .nav .active:hover a,
.nav .nav .active:focus a {
  font-weight: bold;
  padding-left: 30px;
  border-left: 5px solid black;
}

.nav .nav .active span,
.nav .nav .active:hover span,
.nav .nav .active:focus span {
  display: none;
}

Conclusion

Here’s the link to the final demo:

Bootstrap Affix and ScrollSpy Demo

In this article, we’ve seen how to build scrolling effects using Bootstrap’s Affix and ScrollSpy plugins. You’ve probably seen these components in action on Bootstrap’s documentation pages, and now you know how they work.

If you’ve used these components or something similar from a third-party, feel free to let us know about your experiences in the discussion.

Comments
rowild

Thanks, nice article! Could you maybe also elaborate on what to do to implement a url hash change on scrolling? I know there is a

on('activate.bs.scrollspy', function () {

Would that be the way to go? (Maybe with HTML5's push.state?)

georgemarts

Hi @rowild,

This is an interesting question! I think your suggestions make sense. That said, by combining the available event with the HTML5 History API, you should be able to change the url hash.

tjorim

Hi

First of all, thanks for this great guide. On my site: https://jtlm.be/resume.php I have implemented an affix.
However, because I use a navbar-fixed-top, the wrong listitem is selected, any idea how to fix this?
Can't seem to find something on the internet. Thanks in advance.

Kind regards
Jorim

robertdavid01

Great article, very informative, and direct. I used it to identify a bug in a project I was working on. If you use 100% heights for the html & body of your HTML document, the affix plugin will not work correctly.

I posted a question here looking for any solution for using both the heights and the plugin simultaneously.

Stack Overflow Question: Use 100 height with bootstrap affix data-offset-bottom

PaulOB

I assume you have solved it using vh units for the height now and removing the html body rules?

robertdavid01

Thanks @PaulOB that seems to be the best solution, with decent support. The only issue I found was with iOS.

The workaround that I was trying to avoid doing in the end is needed for iOS:
vh iOS workaround

robertdavid01

It seems like an easy solution would be to use the `locaion.hash' javascript property when adding 'active' class.

Location Hash Property`

PaulOB

vh seems to be working in ios 8.3 as far as I can see or was there a specific issue?

robertdavid01

Didnt see anything specific, was just checking caniuse.

Md_Sagor

hei,

    georgemarts thanx for giving your so nice article. I followed your instruction  but my sidebar is not working as your demo site. I think I missed your scripting code. Can you please help me.

my main problem is

  1. sidebar not scrolling
  2. not changing the li classes with page scrolling
  3. i can't doing the javascript code
    Can you elaborate the javascript code for me??
georgemarts

Hi @Md_Sagor Do you have a demo available? Also, I suggest using the demo of this article as a starting point!

Md_Sagor

thank you for your reply.
here is my link http://mdsagorsbd.tk/my/affix%20exercise/affix.html
how to do like you...>>

Axel_Uran

Hi, I used your guide and it was really helpful, the only problem is that the affix won't change to affix-bottom when I arrive to the end of the page. I thought that it might have been because I didn't have a footer, so I added one but still not working.

Here is a demo of my site : http://2015.igem.org/Team:EPF_Lausanne/Test

As you can see, when you scroll to the end of the page the sidebar won't stop. you can also see the css here if you want : http://2015.igem.org/Team:EPF_Lausanne/Test/css?action=raw

Thank you for your time and this great article !
Axel

idleberg

Would be helpful if the pen was (still) working!

Mittineague

Hi idleberg, welcome to the forum

From what I can see, it looks like Bootstrap no longer has the right side navigation it had when this article was publshed in February.

The Codepen linked to in the first post does still work.

Is it not looking / acting like you thought it would or am I missing something?

Recommended
Sponsors
Because We Like You
Free Ebooks!

Grab SitePoint's top 10 web dev and design ebooks, completely free!

Get the latest in Front-end, once a week, for free.