103

I have a site with the navbar fixed on top and 3 divs underneath in the main content area.

I'm trying to use scrollspy from the bootstrap framework.

I have it succesfully highlighting the different headings in the menu when you scroll past the divs.

I also have it so when you click the menu, it will scroll to the correct part of the page. However, the offset is incorrect (It's not taking into account the navbar, so I need to offset by about 40 pixels)

I see on the Bootstrap page it mentions an offset option but i'm not sure how to use it.

I'm also not what it means when it says you can use scrollspy with $('#navbar').scrollspy(), I'm not sure where to include it so I didn't and everything seems to be working (except the offset).

I thought the offset might be the data-offset='10' on the body tag, but it doesn't do anything for me.

I have a feeling that this is something really obvious and I'm just missing it. Any help?

My code is

...
<!-- note: the data-offset doesn't do anything for me -->
<body data-spy="scroll" data-offset="20">
<div class="navbar navbar-fixed-top">
<div class="navbar-inner">
    <div class="container">
    <a class="brand" href="#">VIPS</a>
    <ul class="nav">
        <li class="active">
             <a href="#trafficContainer">Traffic</a>
        </li>
        <li class="">
        <a href="#responseContainer">Response Times</a>
        </li>
        <li class="">
        <a href="#cpuContainer">CPU</a>
        </li>
      </ul>
    </div>
</div>
</div>
<div class="container">
<div class="row">
    <div class="span12">
    <div id="trafficContainer" class="graph" style="position: relative;">
    <!-- graph goes here -->
    </div>
    </div>
</div>
<div class="row">
    <div class="span12">
    <div id="responseContainer" class="graph" style="position: relative;">
    <!-- graph goes here -->
    </div>
    </div>
</div>
<div class="row">
    <div class="span12">
    <div id="cpuContainer" class="graph" style="position: relative;">
    <!-- graph goes here -->
    </div>
    </div>
</div>
</div>

...
<script src="assets/js/jquery-1.7.1.min.js"></script>
<script src="assets/js/bootstrap-scrollspy.js"></script>
</body>
</html>
Mladen Jablanović
  • 43,461
  • 10
  • 90
  • 113
Lango
  • 2,995
  • 5
  • 26
  • 27
  • Why are you giving a position:relative to your divs? Perhaps that's what's causing this. Can you create a jsfiddle with your code so we can see in action? – periklis Feb 15 '12 at 06:45

14 Answers14

117

Bootstrap uses offset to resolve spying only, not scrolling. This means that scrolling to the proper place is up to you.

Try this, it works for me: add an event handler for the navigation clicks.

var offset = 80;

$('.navbar li a').click(function(event) {
    event.preventDefault();
    $($(this).attr('href'))[0].scrollIntoView();
    scrollBy(0, -offset);
});

I found it here: https://github.com/twitter/bootstrap/issues/3316

Kurt UXD
  • 5,776
  • 2
  • 20
  • 16
  • 3
    Thanks for the answer. Took me quite a while to realise that offset does not affect scrolling. This means anybody using a fixed-top needs to manually adjust in this manner. – Brian Smith Aug 06 '12 at 10:49
  • 9
    To update the hash section of the URL appropriately, add `window.location.hash = $(this).attr('href')` to this function. – Stephen M. Harris Oct 30 '12 at 22:01
  • 2
    Thanks! FWIW, putting the data-offset in the body is also important, as the asker says it doesn't do anything, but it is needed for proper spying. It might help to have a comprehensive answer. – mrooney Mar 28 '13 at 15:38
  • 5
    Dynamic win: `var navOffset = $('#navbar').height();` and `scrollBy(0, -navOffset);` – ahnbizcad Jan 29 '15 at 00:26
87

The trick, as Tim alluded to, is to take advantage of padding. So the problem is, your browser always wants to scroll your anchor to the exact top of the window. if you set your anchor where your text actually begins, it will be occluded by your menu bar. Instead, what you do is set your anchor to the container <section> or <div> tag's id, which the nav/navbar will automatically use. Then you have to set your padding-top for the container to the amount offset you want, and the margin-top for the container to the opposite of the padding-top. Now your container's block and the anchor begin at the exact top of the page, but the content inside doesn't begin until below the menu bar.

If you're using regular anchors, you can accomplish the same thing by using negative margin-top in your container as above, but no padding. Then you put a regular <a target="..."> anchor as the first child of the container. Then you style the second child element with an opposite but equal margin-top, but using the selector .container:first-child +.

This all presumes that your <body> tag already has its margin set to begin below your header, which is the norm (otherwise the page would render with occluded text right off the bat).

Here's an example of this in action. Anything on your page with an id can be linked to by tacking #the-elements-id onto the end of your URL. If you make all of your h tags ad dt tags with ids link-able (Like github does) with some sort of script that injects a little link to the left of them; adding the following to your CSS will take care of the nav-bar offset for you:

body {
    margin-top: 40px;/* make room for the nav bar */
}

/* make room for the nav bar */
h1[id],
h2[id],
h3[id],
h4[id],
h5[id],
h6[id],
dt[id]{
    padding-top: 60px;
    margin-top: -40px;
}
cmcculloh
  • 47,596
  • 40
  • 105
  • 130
acjay
  • 34,571
  • 6
  • 57
  • 100
  • 9
    Love the padding-top, negative margin-bottom (on the same element) combo. Easier then messing around with the js. – Eystein Oct 11 '12 at 16:40
  • Looks like the best answer, but I am a little lost. Any chance of an example. – eddyparkinson Apr 23 '14 at 06:17
  • @eddyparkinson, guess you figured it out right now: Example, when you have your id's on the rows of bootstrap: .row[id] { padding-top: 60px; margin-top: -40px; } Put this in your less file: .row[id]{ padding-top: 60px; margin-top: -40px; } – Klaaz Oct 05 '14 at 09:39
  • Isn't the bootstrap's default offset parameter supposed to be for this? It teems like such an obvious thing to include, and not have to resort to workarounds like this. – ahnbizcad Jan 28 '15 at 23:50
  • Does this effectively add extra space on the div you're targeting? What if you don't want that? An invisible extra spacing would be ideal. – ahnbizcad Jan 28 '15 at 23:52
  • I suppose this is the reason why bootstrap's `h*` tags come with an extra 20px top margin. – ahnbizcad Jan 28 '15 at 23:53
  • You beauty, best answer found after messing around with js offsets the whole afternoon :) – Clinton Green Mar 25 '15 at 01:33
  • This seems to be the solution hands-down. It also made it easier to animate scrolling to the position. I had to use a dedicated `div` to wrap the section otherwise it might collide with other CSS rules in Bootstrap. – wigy Jun 20 '16 at 10:35
  • I'm late to this but this solution worked great for me! – Mariton Aug 15 '17 at 01:26
10

You can change the offset of scrollspy on the fly with this (assuming you spying on the body):

offsetValue = 40;
$('body').data().scrollspy.options.offset = offsetValue;
// force scrollspy to recalculate the offsets to your targets
$('body').data().scrollspy.process();

This also works if you need to change the offset value dynamically. (I like the scrollspy to switch over when the next section is about halfway up the window so I need to change the offset during window resize.)

cogell
  • 3,011
  • 1
  • 22
  • 19
  • 2
    Thanks for this - works great with a little adjustment for Bootstrap 3.0 $('body').data()['bs.scrollspy'].options.offset = newOffset; // Set the new offset $('body').data()['bs.scrollspy'].process(); // Force scrollspy to recalculate the offsets to your targets $('body').scrollspy('refresh'); // Refresh the scrollspy. – Sam Nov 11 '13 at 18:56
  • I've found that this method is good, but that if I want to use it to set the offset from the beginning due to having different menu heights the $('body').data().scrollspy.options.offset (or the variant for version 3.0) isn't yet set. Is there something asynchronous about scrollspy that I'm missing? – ness-EE Jan 27 '14 at 08:28
  • As of Bootstrap 3.1.1 this should be `$('body').data()['bs.scrollspy'].options.offset`. Useful for triggering scrollspy on page load by using `window.scrollBy(X,Y)` – Meredith Jul 04 '14 at 07:59
9

If you want an offset of 10 you would put this in your head:

<script>
  $('#navbar').scrollspy({
    offset: 10
  });
</script>

Your body tag would look like this:

<body data-spy="scroll">
David Naffis
  • 161
  • 2
  • 7
    That doesn't offset the content, it offsets the nav. – markus Nov 05 '13 at 00:31
  • This will work for highlighting the correct nav element and not the location where the click leads (which is the original posters question). With that said, this was what I was looking for! Thanks! – valin077 Jun 18 '15 at 04:04
7

From bootstrap issue #3316:

[This] is only intended to modify scrolling calculations - we don't shift the actual anchor click

So setting offset only affects where scrollspy thinks the page is scrolled to. It doesn't affect scrolling when you click an anchor tag.

Using a fixed navbar means that the browser is scrolling to the wrong place. You can fix this with JavaScript:

var navOffset = $('.navbar').height();

$('.navbar li a').click(function(event) {
    var href = $(this).attr('href');

    // Don't let the browser scroll, but still update the current address
    // in the browser.
    event.preventDefault();
    window.location.hash = href;

    // Explicitly scroll to where the browser thinks the element
    // is, but apply the offset.
    $(href)[0].scrollIntoView();
    window.scrollBy(0, -navOffset);
});

However, to ensure that scrollspy highlights the correct current element, you still need to set the offset:

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

Here's a complete demo on JSFiddle.


Alternatively, you could padding to your anchor tags, as suggested by CSS tricks.

a[id]:before { 
  display: block; 
  content: " ";
  // Use the browser inspector to find out the correct value here.
  margin-top: -285px; 
  height: 285px; 
  visibility: hidden; 
}
Wilfred Hughes
  • 29,846
  • 15
  • 139
  • 192
  • i had to add :not(:first-of-type) `[id]:not(:first-of-type):before {` because in my case first element overlaps header on mobile – okliv May 14 '22 at 13:18
1

I think that offset might do something with the bottom position of the section.

I fixed my issue - the same as yours - by adding

  padding-top: 60px;

in the declaration for section in bootstrap.css

Hope that helps!

Botz3000
  • 39,020
  • 8
  • 103
  • 127
Tim
  • 19
  • 2
1

I added 40px-height .vspace element holding the anchor before each of my h1 elements.

<div class="vspace" id="gherkin"></div>
<div class="page-header">
  <h1>Gherkin</h1>
</div>

In the CSS:

.vspace { height: 40px;}

It's working great and the space is not chocking.

Quentin
  • 2,529
  • 2
  • 26
  • 32
1

Bootstrap 4 (2019 update)

Implementing: Fixed Nav, Scrollspy, and Smooth Scrolling

This is the solution from @wilfred-hughes above, it exactly fixes the overlap issue with the Bootstrap Fixed Navbar by using CSS '::before' to prepend an non-displayed block before each section tag. Advantage: you don't end up with unnecessary padding between your sections. Eg, section 3 can be vertically positioned right up against section 2 and it will still scroll to the exact correct position at the top of section 3.

Side note: setting the scrollspy offset in the script tag caused nothing to happen ( $('#navbar').scrollspy({ offset: 105 });) and attempts to adjust the offset in the animation block still resulted in misalignment post-animation.

index.html

<section id="section1" class="my-5">
...
<section id="section2" class="my-5">
...
<section id="section3" class="my-5">
...

later in index.html...

 <script>
      // Init Scrollspy
      $('body').scrollspy({ target: '#main-nav' });

      // Smooth Scrolling
      $('#main-nav a').on('click', function(event) {
        if (this.hash !== '') {
          event.preventDefault();

          const hash = this.hash;

          $('html, body').animate(
            {
              scrollTop: $(hash).offset().top
            },
            800,
            function() {
              window.location.hash = hash;
            }
          );
        }
      });
    </script>

style.css:

// Solution to Fixed NavBar overlapping content of the section.  Apply to each navigable section.
// adjust the px values to your navbar height
// Source: https://github.com/twbs/bootstrap/issues/1768
#section1::before,
#section2::before,
#section3::before {
  display: block;
  content: ' ';
  margin-top: -105px;
  height: 105px;
  visibility: hidden;
}

body {
  padding-top: 105px;
}

Credit: @wilfred-hughes above and the original source from user @mnot at https://github.com/twbs/bootstrap/issues/1768

ObjectiveTC
  • 2,477
  • 30
  • 22
0

I had problems with the solutions of acjohnson55 and Kurt UXD. Both worked for clicking a link to the hash, but the scrollspy offset was still not correct.

I came up with the following solution:

<div class="anchor-outer">
  <div id="anchor">
    <div class="anchor-inner">
      <!-- your content -->
    </div>
  </div>
</div>

And the corresponding CSS:

body {
  padding-top:40px;
}

.anchor-outer {
  margin-top:-40px;
}

.anchor-inner {
  padding-top:40px;
}

@media (max-width: 979px) {
  body {
    padding-top:0px;
  }

  .anchor-outer {
    margin-top:0px;
  }

  .anchor-inner {
    padding-top:0px;
  }
}

@media (max-width: 767px) {
  body {
    padding-top:0px;
  }

  .anchor-outer {
    margin-top:0px;
  }

  .anchor-inner {
    padding-top:0px;
  }
}

You will need to replace the 40px padding / margin with the height of your navbar, and the id of your anchor.

In this setup I can even observe an effect of setting a value for the data-offset attribute. If find large values for the data-offset around 100-200 lead to a more expected behavior (the scrollspy-"switch" will happen if the heading is circa in the middle of the screen).

I also found that scrollspy is very sensitive to errors like unclosed div tags (which is quite obvious), so http://validator.w3.org/check was my friend here.

In my opinion scrollspy works best with whole sections carrying the anchor ID, since regular anchor elements do not lead to expected effects when scrolling backwards (the scrollspy-"switch" eventually happens as soon as you hit the anchor element, e.g. your heading, but at that time you have already scrolled over the content which the user expects to belong to the according heading).

schreon
  • 1,097
  • 11
  • 25
0

Take a look on this post I made in another thread. This contains a snippet on how you can programmatically change the offset value and dynamically react on changes (e.g. if the navbar changes in size).

You don't need to tinker around with CSS fixes but rather set the offset you currently need in dependence of certain events.

isherwood
  • 58,414
  • 16
  • 114
  • 157
thex
  • 590
  • 2
  • 12
0

In Bootstrap v5.1.3 I have used data-bs-offset="xxx" on the body element in my project and it works perfectly.

Example:

<body data-bs-spy="scroll" 
      data-bs-target="#navbar-scrollspy" 
      data-bs-offset="100">                 <<== see here

<nav class="navbar fixed-top" id="navbar-scrollspy">
    ...
</nav>

...

</body>
Jayme
  • 1,776
  • 10
  • 28
0

NO CSS and content elements need to be changes. Try this code:

$(document).ready(function () {    
    nav_top = $("#tsnav"); 
    offset = 10; // get extra space
    side_nav = $("#cve_info") // functional nav
    top_height = nav_top.outerHeight() + offset; // get the top navbar hieght + get extra space

side_nav.find("a").on('click', function () {
    let $el = $(this);
        id = $el.attr("href");

    $('html, body').scrollTop( $(id).offset().top - top_height);
    return false;
})

});

-1

You can also open bootstap-offset.js and modify

$.fn.scrollspy.defaults = {
  offset: 10
}

there.

Adobe
  • 12,967
  • 10
  • 85
  • 126
-2

Just set :

data-offset="200"

data-offset is by default "50" and that is equivalent 10px, so just increase data-offset and your problem is solved.

cakan
  • 2,099
  • 5
  • 32
  • 42