Reading Progress Indicator

Reading Progress Indicator

A widget containing a list of suggested articles, with a reading progress indicator powered by SVG, CSS and jQuery.

Nucleo icons

Sponsored by Nucleo, a free application to collect, customize and export all your icons as icon font and SVG symbols. Made by the CodyHouse folks!

Do you want to include this resource in a product offered for sale? Learn more about our Extended License

Today’s resource was inspired by a widget found on The Daily Beast: a list of related articles, enriched by a filling effect to indicate the reading progress. We created something similar, although we used SVG to animate the stroke property of a circle element.

Note that the url changes according to the article in focus, in case the user wants to share a specific article as opposed to the whole page.

Since such a widget is not a fundamental element of the page, but more of a subtle enrichment, we decided to hide it on smaller devices.

Creating the structure

The HTML structure is composed by <article> elements for the article contents, and an <aside> element wrapping the list of suggested articles.

<div class="cd-articles">
   <article>
      <header>
         <img src="img/img-1.png" alt="article image">
         <h1>20 Star Wars Secrets Revealed: From Leia’s ‘Cocaine Nail’ to the Ronald Reagan Connection</h1>
      </header>

      <p>
         Lorem ipsum dolor sit amet, consectetur adipisicing elit. Perferendis maxime id, sunt, eum sed blanditiis aliquid! Minus assumenda tempore perspiciatis, numquam est aliquam, quis molestias enim consequuntur suscipit similique cumque ut natus facilis laboriosam quidem, nesciunt quasi doloribus tenetur. Quas doloremque suscipit, molestias odit, et quasi? Quas hic numquam, vitae?
      </p>
      <!-- additional content here -->
   </article>

   <article>
      <!-- article content here -->
   </article>

   <!-- additional articles here -->

   <aside class="cd-read-more">
      <ul>
         <li>
            <a href="index.html">
               <em>20 Star Wars Secrets Revealed</em>
               <b>by J. Morrison</b>
               <svg x="0px" y="0px" width="36px" height="36px" viewBox="0 0 36 36"><circle fill="none" stroke="#2a76e8" stroke-width="2" cx="18" cy="18" r="16" stroke-dasharray="100 100" stroke-dashoffset="100" transform="rotate(-90 18 18)"></circle></svg>
            </a>
         </li>

         <!-- additional links to articles -->
      </ul>
   </aside> <!-- .cd-read-more -->
</div> <!-- .cd-articles -->

Adding style

The <aside> element is visible only on big devices (viewport width bigger than 1100px): it has an absolute position and is placed in the top-right corner of the .cd-articles element; the class .fixed is then used to change its position to fixed so that it's always accessible while the user scrolls through the articles.

@media only screen and (min-width: 1100px) {
  .cd-articles {
    position: relative;
    width: 970px;
    padding-right: 320px;
  }
}

.cd-read-more {
  /* hide on mobile */
  display: none;
}
@media only screen and (min-width: 1100px) {
  .cd-read-more {
    display: block;
    width: 290px;
    position: absolute;
    top: 3em;
    right: 0;
  }
  .cd-read-more.fixed {
    position: fixed;
    right: calc(50% - 485px);
  }
}

To create the progress effect, we used the two svg attributes stroke-dasharray and stroke-dashoffset. Imagining the circle as a dashed line, the stroke-dasharray lets you specify dashes and gaps length, while the stroke-dashoffset lets you change where the dasharray starts. We initially set stroke-dasharray="100 100" and stroke-dashoffset="100" (where 100 is the svg circle circumference). This way, the dash and gap are both equal to the circle circumference, and since the stroke-dashoffset is equal to the circle circumference too, only the gap (transparent) is visible. To create the progress effect, we change the stroke-dashoffset from 100 to 0 (more in the Events Handling section).

Events handling

On big devices (viewport width bigger than 1100px) we bind the updateArticle() and updateSidebarPosition() functions to the window scroll events: the first one checks which article the user is reading and updates the corresponding svg stroke-dashoffset attribute to show the progress, while the second function updates the sidebar position attribute (using the .fixed class ) according to the window scroll top value. Finally, the changeUrl() function is used to update the page url according to the article being read.

function updateArticle() {
   var scrollTop = $(window).scrollTop();

   articles.each(function(){ //articles = $('.cd-articles').children('article');
      var article = $(this),
          articleSidebarLink = articleSidebarLinks.eq(article.index()).children('a'); //articleSidebarLinks = $('.cd-read-more').find('li')

      if( articleTop > scrollTop) { //articleTop = $(this).offset().top
         articleSidebarLink.removeClass('read reading');
      } else if( scrollTop >= articleTop && articleTop + articleHeight > scrollTop) { //articleHeight = $(this).outerHeight()
         var dashoffsetValue = svgCircleLength*( 1 - (scrollTop - articleTop)/articleHeight); //svgCircleLength = 100
         articleSidebarLink.addClass('reading').removeClass('read').find('circle').attr({ 'stroke-dashoffset': dashoffsetValue });
         changeUrl(articleSidebarLink.attr('href'));
      } else {
         articleSidebarLink.removeClass('reading').addClass('read');
      }
   });
}

function updateSidebarPosition() {
   var scrollTop = $(window).scrollTop();

   if( scrollTop < articlesWrapperTop) { //$('.cd-articles').offset().top
      aside.removeClass('fixed').attr('style', ''); //aside = $('.cd-read-more')
   } else if( scrollTop >= articlesWrapperTop && scrollTop < articlesWrapperTop + articlesWrapperHeight - windowHeight) { // articlesWrapperHeight = $('.cd-articles').outerHeight()
      aside.addClass('fixed').attr('style', '');
   } else {
      if( aside.hasClass('fixed') ) aside.removeClass('fixed').css('top', articlesWrapperHeight + articlePaddingTop - windowHeight + 'px');//articlePaddingTop = Number($('.cd-articles').children('article').eq(1).css('padding-top').replace('px', ''))
   }
}

Join our newsletter

Get our monthly recap with the latest CodyHouse news