Animated Side TOC Nav For Long Web Content

Category: Javascript , Menu & Navigation , Recommended | January 13, 2017
Author: hakimel
Views Total: 1,508
Official Page: Go to website
Last Update: January 13, 2017
License: MIT

Preview:

Animated Side TOC Nav For Long Web Content

Description:

This is a pretty cool side TOC (table of contents) & navigation system which allows to highlight the current nav item with an animated progress line when you scroll through the webpage.

How to use it:

Create the TOC & Navigation together with the corresponding content sections and SVG based progress bar on the webpage as follows:

<nav class="toc">
  <ul>
    <li><a href="#section1">Section One</a></li>
    <li>
      <a href="#section2">Section Two</a>
      <ul>
        <li><a href="#section21">Section 2-1</a></li>
        <li><a href="#section22">Section 2-2</a></li>
        <li><a href="#section23">Section 2-3</a></li>
      </ul>
    </li>
  </ul>
  <svg class="toc-marker" width="200" height="200" xmlns="http://www.w3.org/2000/svg">
    <path stroke="#444" stroke-width="3" fill="transparent" stroke-dasharray="0, 0, 0, 1000" stroke-linecap="round" stroke-linejoin="round" transform="translate(-0.5, -0.5)" />
  </svg>
</nav>
<article class="contents">

  <section id="section1">
    <h2>Section One</h2>
  </section>

  <section>

    <div id="section2">
      <h2>Section Two</h2>
    </div>

    <div id="section21">
      <h3>Section 2-1</h3>
    </div>

    <div id="section22">
      <h3>Section 2-2</h3>
    </div>

 </section>

 ......

</article>

Style the TOC & Navigation.

.toc {
  position: fixed;
  left: 3em;
  top: 5em;
  padding: 1em;
  width: 14em;
  line-height: 2;
}

.toc ul {
  list-style: none;
  padding: 0;
  margin: 0;
}

.toc ul ul { padding-left: 2em; }

.toc li a {
  display: inline-block;
  color: #aaa;
  text-decoration: none;
  -webkit-transition: all 0.3s cubic-bezier(0.23, 1, 0.32, 1);
  transition: all 0.3s cubic-bezier(0.23, 1, 0.32, 1);
}

.toc li.visible > a {
  color: #111;
  -webkit-transform: translate(5px);
  transform: translate(5px);
}

.toc-marker {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  z-index: -1;
}

.toc-marker path {
  -webkit-transition: all 0.3s ease;
  transition: all 0.3s ease;
}

The main JavaScript:

var toc = document.querySelector( '.toc' );
var tocPath = document.querySelector( '.toc-marker path' );
var tocItems;

// Factor of screen size that the element must cross
// before it's considered visible
var TOP_MARGIN = 0.1,
    BOTTOM_MARGIN = 0.2;

var pathLength;

window.addEventListener( 'resize', drawPath, false );
window.addEventListener( 'scroll', sync, false );

drawPath();

function drawPath() {
  
  tocItems = [].slice.call( toc.querySelectorAll( 'li' ) );

  // Cache element references and measurements
  tocItems = tocItems.map( function( item ) {
    var anchor = item.querySelector( 'a' );
    var target = document.getElementById( anchor.getAttribute( 'href' ).slice( 1 ) );

    return {
      listItem: item,
      anchor: anchor,
      target: target
    };
  } );

  // Remove missing targets
  tocItems = tocItems.filter( function( item ) {
    return !!item.target;
  } );

  var path = [];
  var pathIndent;

  tocItems.forEach( function( item, i ) {

    var x = item.anchor.offsetLeft - 5,
        y = item.anchor.offsetTop,
        height = item.anchor.offsetHeight;

    if( i === 0 ) {
      path.push( 'M', x, y, 'L', x, y + height );
      item.pathStart = tocPath.getTotalLength() || 0;
    }
    else {
      // Draw an additional line when there's a change in
      // indent levels
      if( pathIndent !== x ) path.push( 'L', pathIndent, y );

      path.push( 'L', x, y );
      
      // Set the current path so that we can measure it
      tocPath.setAttribute( 'd', path.join( ' ' ) );
      item.pathStart = tocPath.getTotalLength() || 0;
      
      path.push( 'L', x, y + height );
    }
    
    pathIndent = x;
    
    tocPath.setAttribute( 'd', path.join( ' ' ) );
    item.pathEnd = tocPath.getTotalLength();

  } );
  
  pathLength = tocPath.getTotalLength();
  
  sync();
  
}

function sync() {
  
  var windowHeight = window.innerHeight;
  
  var pathStart = Number.MAX_VALUE,
      pathEnd = 0;
  
  var visibleItems = 0;
  
  tocItems.forEach( function( item ) {

    var targetBounds = item.target.getBoundingClientRect();
    
    if( targetBounds.bottom > windowHeight * TOP_MARGIN && targetBounds.top < windowHeight * ( 1 - BOTTOM_MARGIN ) ) {
      pathStart = Math.min( item.pathStart, pathStart );
      pathEnd = Math.max( item.pathEnd, pathEnd );
      
      visibleItems += 1;
      
      item.listItem.classList.add( 'visible' );
    }
    else {
      item.listItem.classList.remove( 'visible' );
    }
    
  } );
  
  // Specify the visible path or hide the path altogether
  // if there are no visible items
  if( visibleItems > 0 && pathStart < pathEnd ) {
    tocPath.setAttribute( 'stroke-dashoffset', '1' );
    tocPath.setAttribute( 'stroke-dasharray', '1, '+ pathStart +', '+ ( pathEnd - pathStart ) +', ' + pathLength );
    tocPath.setAttribute( 'opacity', 1 );
  }
  else {
    tocPath.setAttribute( 'opacity', 0 );
  }

}

You Might Be Interested In:

Leave a Reply