December 21, 2015 | 25 Feedbacks

Reading Progress Indicator

A widget containing a list of suggested articles, with a reading progress indicator powered by SVG, CSS and jQuery.
Browser support
  • ie
  • Chrome
  • Firefox
  • Safari
  • Opera

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.

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.

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.


Dec 21, 2015
  • Resource released by CodyHouse

Sebastiano Guerriero

UI/UX designer, with a huge passion for Nutella. Co-Founder of CodyHouse. You can follow him on Twitter or Dribbble.

  • Florian

    This gives horrible performance! You should just bind a boolean to the scroll event has_scrolled=true and then do all the hard work in a requestAnimationFrame function.

    • Claudia Romano

      Hi Florian, thanks for your feedback.
      Actually, we are not always able to explain all the details of the resource in our article (mostly,we try to highlight the main points). But if you take a look at the full main.js file, you can see that we actually use the requestAnimationFrame as you suggested. Cheers!

    • Quentin Gille

      Well not just that, the “previous page” button, on Google Chrome can’t handle this, hehe.
      But I like the idea, anyway

  • Optimix

    Thank you for another great article Sebastiano! :)
    URl is changed automatically. But I’ve noticed that after refreshing the page when you on 3rd item, for example, it is not highlighted as current item. Can you please have a look at this issue? Thanks again!

    • Sebastiano Guerriero

      Hi there! Just fixed this bug, thanks for the heads up

  • Dani Marti

    Wow! Really cool!! Can’t wait to use it in my next project :-)

  • Manny Fleurmond

    I definitely see this having bad performance due to the event callbacks being called nonstop while scrolling. I suggest using a debouncing function, which will delay when the callbacks get called and improve performance. Here is the debounce function taken from the underscore.js library:

    // Returns a function, that, as long as it continues to be invoked, will not
    // be triggered. The function will be called after it stops being called for
    // N milliseconds. If immediate is passed, trigger the function on the
    // leading edge, instead of the trailing.
    function debounce(func, wait, immediate) {
    var timeout;
    return function() {
    var context = this, args = arguments;
    var later = function() {
    timeout = null;
    if (!immediate) func.apply(context, args);
    var callNow = immediate && !timeout;
    timeout = setTimeout(later, wait);
    if (callNow) func.apply(context, args);

    Then you just do this for the events:

    $(window).on(‘scroll’, debounce( checkRead, 500 ) );

    More about debouncing:

    • Claudia Romano

      Hi Manny, thanks for your feedback. If you take a look at the complete main.js file, you can see that the events callback is not called nonstop, but we rather use the requestAnimationFrame function ( combined with a scrolling variable for the debounce). You can learn more about this technique here:

  • Francis Kim

    Great work Sebastiano!

  • Timur

    Hi Sebastiano & Claudia!
    This works and looks great! There can be so many uses for it. Thanks for sharing it!
    I have a question: is it possible to use id selectors to navigate to instead of url? (e.g #item-1, #item-2).
    I have an idea to use it for a restaurant menu (wine-list) where it would highlight a current name of the wine being in viewport area as user scrolls down to different wines.
    Thank you!

    • Claudia Romano

      Hi Timur, you should try replacing the href of the .cd-read-more > a elements with the new page url + id selectors, and add the proper id to each element. Hope this helps!

      • Timur

        Hi Claudia! It does help perfectly! Thank you! :)

  • Anish M Alias

    Nice one

  • david

    Loved this article! But may I propose that you change

    (scrollTop – articleTop)/articleHeight

    to something

    scrolledInPercent = scrollTop / (scrollHeight – winHeight);
    ((scrollTop + (winHeight * scrolledInPercent)) – articleTop)/articleHeight

    So you would make sure that even if the article is very short it works accurately.

    used something similar here:

    • Idris Belaoul

      Nice one :) thx

    • JDWolt

      How would you apply this to the current modal?

  • Olivia F. Gresham

    Nice! Imagine if they started to use this for contracts that people have to “Read through”.

  • Gonzalo

    Very nice, guys! As always!
    Just a little add-on: if you add the following “transition: all 0.5s ease;” to the , you’ll get a smooth animation on these little circles :3

  • Alex Fedotov
    • Claudia Romano

      Great job! ;)

  • Muhammad Ibnuh


  • 1eddy87

    Hey guys, really nice design.

    I was thinking of using this in a project that I’m working on.

    If I was to use it, it would be for my documentation, am I required to buy a license for this?

    Nothing is being sold on the site.

    • Claudia Romano

      Hi there,
      please take a look at our Terms:

      As for the SVG, we animate the stroke-dashoffset attribute (more info in the article)

  • Soxikl

    Your code is sale on this website by another user…

    • Claudia Romano

      Thanks for the heads-up! It’s been removed now ;)