Search code examples
javascriptjqueryhtmlnavsticky

Sticky Header - Scroll with Tabs


I am having problems with the behavior of my sticky header.

Desired behavior:

a) Scrolling to where the bottom of the .nav hits the top of a section adds the active class to the tab and it remains active until the .nav reaches the top of the next section.

b) Clicking a corresponding section's .tab always navigates you to the top of that section and adds the active class to the tab.

So either by scrolling or clicking, the active state of the tabs is always kept until the .nav crosses into the next section, in which case the active state goes to that section's tab, etc.

Test the issues:

1) Half-way scrolling down Option Two the active state of that .tab is lost.

2) The use of scrollTop is scrolling to the top of the .container instead of the top of the selected section.

class StickyNavigation {
  constructor() {
    this.currentId = null;
    this.currentTab = null;
    let self = this;
    $(".tab").click(function() {
      self.onTabClick(event, $(this));
    });
    $(".container").scroll(() => {
      this.onScroll();
    });
    $(".container").resize(() => {
      this.onResize();
    });
  }
  /*Scrolls down to Tab selection*/
  onTabClick(event, element) {
    event.preventDefault();
    let scrollTop = $(element.attr("href")).offset().top;
    if (!$(".nav").hasClass("nav--top")) {
      scrollTop = scrollTop;
    }
    $(".container").animate({
      scrollTop: scrollTop
    }, 600);
  }
  onScroll() {
    this.navPosition();
    this.tabAnimation();
  }
  navPosition() {
    let offset = $(".sticky").offset().top + $(".sticky").height();
    if ($(".container").scrollTop() > offset) {
      $(".nav").addClass("nav--top");
    } else {
      $(".nav").removeClass("nav--top");
    }
  }
  tabAnimation() {
    $("section").each(function() {
      var actual = $(this),
        actualHeight = actual.height(),
        actualAnchor = $(".sticky").find('a[href="#' + actual.attr("id") + '"]');
      if (
        actual.offset().top <= $(".container").scrollTop() &&
        actual.offset().top + actualHeight > $(".container").scrollTop()
      ) {
        actualAnchor.addClass("active");
      } else {
        actualAnchor.removeClass("active");
      }
    });
  }
}
new StickyNavigation();
body {
  position: fixed;
  display: flex;
  flex-direction: column;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  overflow: hidden;
}

section {
  height: 600px;
  border: 2px solid white;
  background: blue;
}

section:nth-child(2) {
  background: red;
}

.container {
  flex: 1;
  display: flex;
  position: relative;
  flex-direction: column;
  overflow: auto;
}

.long {
  height: 1200px;
}

.header {
  height: 75px;
  background: green;
}

.hero {
  background: silver;
  flex: 0;
  border: 1px solid;
}

.nav {
  background: white;
  width: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
  position: relative;
}

.nav--top {
  position: fixed;
  top: 75px;
}

.sticky {
  background: white;
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  position: relative;
}

.tab {
  padding: 30px 45px;
  position: relative;
}

.tab.active {
  background: #6567c5;
  color: white;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class="header">
  <h1>Header</h1>
</div>
<div class="container">
  <div class="hero">
    <h1>Hero</h1>
  </div>
  <div class="sticky">
    <nav role="navigation" class="nav">
      <a class="tab" href="#One">Option One</a>
      <a class="tab" href="#Two">Option Two</a>
    </nav>
  </div>
  <div class="main">
    <section id="One">
    </section>
    <section class="long" id="Two">
    </section>
  </div>
</div>
</div>


Solution

  • The reason your section scrolls to the top is the change of positions due to adding and removal sticky element. Use a wrapper in place of sticky to always occupy the height of the sticky element whether it is there or not.

    class StickyNavigation {
      constructor() {
        this.currentId = null;
        this.currentTab = null;
        this.setup();
        this.onResize();
        let self = this;
        $(".tab").click(function(event) {
          self.onTabClick(event, $(this));
        });
        $(".container").scroll(() => {
          this.onScroll();
        });
        $(window).resize(() => {
          this.onResize();
        });
      }
      setup() {
        this.$sticky = $('.sticky');
        window.stk = this.$sticky;
        this.$stickyWrap = $('<div>').insertAfter(this.$sticky);
        this.$sticky.appendTo(this.$stickyWrap);
      }
      /*Scrolls down to Tab selection*/
      onTabClick(event, element) {
        event.preventDefault();
        let $targetElement = $(element.attr("href"));
        let positionTop = $targetElement.position().top;
        let scrollTop = $('.container').scrollTop();
        $(".container").animate({
          scrollTop: scrollTop - this.stickyOuterHeight + positionTop
        }, 600);
      }
      onScroll() {
        this.navPosition();
        this.tabAnimation();
      }
      onResize() {
        this.stickyOuterHeight = this.$sticky.outerHeight();
        this.$sticky.width(this.$stickyWrap.width());
        this.$stickyWrap.css('minHeight', this.stickyOuterHeight);
      }
      navPosition() {
        if (this.$stickyWrap.position().top < 0) {
          this.$sticky.addClass("fixed");
        } else {
          this.$sticky.removeClass("fixed");
        }
      }
      tabAnimation() {
        let desiredSpace = this.stickyOuterHeight + 10;
        $("section").each(function() {
          let actual = $(this),
            actualHeight = actual.height(),
            actualAnchor = $(".sticky").find('a[href="#' + actual.attr("id") + '"]');
          let actualTop = actual.position().top;
          let actualBottom = actualTop + actualHeight;
          if (actualTop < desiredSpace && actualBottom > desiredSpace) {
            actualAnchor.addClass("active");
          } else {
            actualAnchor.removeClass("active");
          }
        });
      }
    }
    $(function() {
      new StickyNavigation();
    });
    body {
      position: fixed;
      display: flex;
      flex-direction: column;
      top: 0;
      bottom: 0;
      left: 0;
      right: 0;
      overflow: hidden;
    }
    
    section {
      height: 600px;
      border: 2px solid white;
      background: blue;
    }
    
    section:nth-child(2) {
      background: red;
    }
    
    .container {
      flex: 1;
      display: flex;
      position: relative;
      flex-direction: column;
      overflow: auto;
    }
    
    .long {
      height: 1200px;
    }
    
    .header {
      height: 75px;
      background: green;
    }
    
    .hero {
      background: silver;
      flex: 0;
      border: 1px solid;
    }
    
    .nav {
      background: white;
      width: 100%;
      display: flex;
      justify-content: center;
      align-items: center;
      position: relative;
    }
    
    .sticky {
      display: flex;
      flex-direction: row;
      flex-wrap: wrap;
    }
    
    .sticky.fixed {
      position: fixed;
      top: 83px;
    }
    
    .tab {
      padding: 30px 45px;
      position: relative;
    }
    
    .tab.active {
      background: #6567c5;
      color: white;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <div class="header">
      <h1>Header</h1>
    </div>
    <div class="container">
      <div class="hero">
        <h1>Hero</h1>
      </div>
      <div class="sticky">
        <nav role="navigation" class="nav">
          <a class="tab" href="#One">Option One</a>
          <a class="tab" href="#Two">Option Two</a>
        </nav>
      </div>
      <div class="main">
        <section id="One">
          Content One
        </section>
        <section class="long" id="Two">
          Content Two
        </section>
      </div>
    </div>