Reading Progress Indicator

Go to Source

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

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">
			<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>
			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?
		<!-- additional content here -->

		<!-- article content here -->

	<!-- additional articles here -->

	<aside class="cd-read-more">
				<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>

			<!-- additional links to articles -->
	</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 });
		} else {

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', ''))

Go to Source