Bootstrap affix navbar for single page with scrollspy and page anchors

thomaslissajoux picture thomaslissajoux · Sep 14, 2013 · Viewed 11.7k times · Source

This is for a single page, with a navbar that links to local anchors only.
The navbar comes after a header, but sticks to top when scrolling down.

You can see how it works on github pages

But I've got two offset problems with link/anchors:

  • as long as you don't scroll, the anchors are offset and masked by the navbar.
  • once the navbar is affixed, the following links work as intended but not the first one.

A body margin breaks the layout as it prevents the header from beginning right at the top:

body {
    margin-top: 65px;
}

I've tried without success to play with margin/padding for the sections:

section {
    padding-top: 65px;
    margin-top: -65px; 
}

Here are the html and css

  • Any idea how to fix that?
  • Can it be solved with pure css?
  • Or do I need some js fix to account for the affix?

Answer

Bass Jobsen picture Bass Jobsen · Sep 16, 2013

I think your problem has only to do with the affix. I found a problem in 3 situations:

  1. no scroll and clicking a link
  2. click the first link
  3. scoll, click the first link and click an other link.

In this three situation you click from an position where you affix is not applied to a position where your affix has been applied.

What happens your click scrolls the target anchor to the top of the page and applies the affix (set navbar's position to fixed) after this. Result the navbar overlaps the content.

I don't think you could fix this with css only. I think your solution of adding a margin / padding to the section will be right, but you will have to apply the margin after the affix.

I tried something like:

var tmp = $.fn.affix.Constructor.prototype.checkPosition;
var i = 0;
var correct = false
$.fn.affix.Constructor.prototype.checkPosition = function () {
  $('#content').css('margin-top','0');
  tmp.call(this);

  if(i%2!=0 && $(window).scrollTop()<443){correct=true}
  if(i%2==0 && correct){$('#content').css('margin-top','83px').trigger('create'); correct=false}
  i++;
}

This feels to complex and also only seems to work on firefox now.

update

I think i could fix your problem by overwritting the complete affix checkPosition function:

$.fn.affix.Constructor.prototype.checkPosition = function () 
{
    if (!this.$element.is(':visible')) return

    var scrollHeight = $(document).height()
    var scrollTop    = this.$window.scrollTop()
    var position     = this.$element.offset()
    var offset       = this.options.offset
    var offsetTop    = offset.top
    var offsetBottom = offset.bottom
    if(scrollTop==378) 
    {
    this.$window.scrollTop('463');
    scrollTop==463;
    }
    if (typeof offset != 'object')         offsetBottom = offsetTop = offset
    if (typeof offsetTop == 'function')    offsetTop    = offset.top()
    if (typeof offsetBottom == 'function') offsetBottom = offset.bottom()

    var affix = this.unpin   != null && (scrollTop + this.unpin <= position.top) ? false :
                offsetBottom != null && (position.top + this.$element.height() >= scrollHeight - offsetBottom) ? 'bottom' :
                offsetTop    != null && (scrollTop <= offsetTop) ? 'top' : false
    console.log(scrollTop + ':' + offsetTop);

    if(scrollTop > offsetTop) {$('#content').css('margin-top','83px'); console.log('margin') }
    else{$('#content').css('margin-top','0');}
    if (this.affixed === affix) return

    if (this.unpin) this.$element.css('top', '')

    this.affixed = affix
    this.unpin   = affix == 'bottom' ? position.top - scrollTop : null

    this.$element.removeClass('affix affix-top affix-bottom').addClass('affix' + (affix ? '-' + affix : ''))

    if (affix == 'bottom') {
      this.$element.offset({ top: document.body.offsetHeight - offsetBottom - this.$element.height() })
    }
}

Some values are hard coded (now) so this function only will work for your example on github pages.

Demo: http://bootply.com/81336

On github pages you use "old" versions of jQuery and Bootstrap. You don't need to set an offset for the scrollspy. You don't have to call $('#navbar').scrollspy(); also cause you already set the scrollspy with data attributes.

See also: https://github.com/twbs/bootstrap/issues/10670

remove this hardcode values

When clicking an internal link (start with #{id}) the anchor with id={id} will be scrolled to the top of the viewport. In this case there will be a fixed navbar (affix) so the anchor should scroll to the top minus the height of the navbar. The height of the navbar will be 85px (63 pixels of the brand image + 2 pixels of the border + the margin-bottom of 20 px of the .navbarheader)

This value will be used here:

if(scrollTop > offsetTop) {$('#content').css('margin-top','83px'); console.log('margin') }
else{$('#content').css('margin-top','0');}

I have used 83 (may look better?). So the 83 can be replaced with: var navbarheight = $('#nav').innerHeight()

Then we have these:

if(scrollTop==378) 
{
this.$window.scrollTop('463');
scrollTop==463;//typo?? make no sense
} 

The (first) link scrolls the anchor to the top where the affix is not applied yet (below data-offset-top="443") the height of your fixed navbar is not used in calculacting so this point will be 443 - 85 (navbarheight) = 378. This code could be replace with.

if(scrollTop==(443-navbarheight)) 
{
this.$window.scrollTop(scrollTop+navbarheight);
}

Note 443 now still will be hardcoded. It is also hardcoded in your html with affix-top.

Watch out Replacing the values with the above won't work. The situation between (af)fixed and not will change for every scroll action. The part if(scrollTop==378) is a trick not a solution. It solves the situation for scrollheight < data-offset-top. We could not apply the whole range, case in that case the user can't never scroll back to the top (this.$window.scrollTop scrolls him back again and again).

Also the calculation of navbarheight will be tricky. When the navbar is fixed $('#nav').innerHeight() / height will return 85 (including the margin). In the absolute position this will be 65.