1. Web Design
  2. HTML/CSS
  3. SVG

How to Build a Page Scroll Progress Indicator With jQuery and SVG

Scroll to top

Today we will be looking at a few techniques we can use to show scroll progress for users who are reading a page. This technique is being used on an increasing number of sites, and for good reason; it provides a contextual understanding of investment needed to consume a particular page. As the user scrolls, they are presented with a sense of current progress in different formats. 

As seen on ia.net

Today, we will cover two specific techniques you can employ to show scroll progress, and leave you with a toolset to create your own. Let's get started!

Setting up the Document

First, we will set up a mock document which will act as our post page. We will be using normalize.css and jQuery, as well as a Google font. Your empty HTML file should look like this:

1
<!doctype html>
2
<html>
3
    <head>
4
        <title>Progress Indicator Animation</title>
5
        <link rel="stylesheet" href="css/normalize.css">
6
        <link rel="stylesheet" href="css/style.css">
7
    </head>
8
    <body>
9
        <!-- fake post content goes here -->
10
        <script src="js/jquery.min.js"></script>
11
        <script src="js/script.js"></script>
12
    </body>
13
</html>

Next, we will add our fake post content:

1
<main>
2
    <article>
3
        <header>
4
            <h1>
5
                <div class="container">
6
                    How Should We Show Progress While Scrolling a Post?
7
                </div>
8
            </h1>
9
        </header>
10
        <div class="article-content">
11
                <h2 class="lead-in">
12
                    <div class="container">
13
                        Lorem ipsum dolor sit amet, consectetur adipisicing elit.
14
                    </div>
15
                </h2>
16
            <div class="container">
17
                <p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus</p>
18
                <!-- add your own additional lorem here -->
19
            </div>
20
        </div>
21
    </article>
22
    <footer>
23
        <h3 class="read-next"><small>Read Next:</small><br>How do I Implement a Foobar?</h3>
24
    </footer>
25
</main>

This gives us enough content to test our scrolling behaviors.

Basic Styling

We're going to use some basic styling to make our post a little more attractive.

1
@import url(http://fonts.googleapis.com/css?family=Domine:400,700);
2
body {
3
    font-size: 16px;
4
}
5
h1,
6
h2,
7
h3,
8
h4,
9
h5,
10
h6 {
11
    font-family: "Domine", sans-serif;
12
}
13
14
h1 {
15
    font-size: 3.5em;
16
}
17
18
.lead-in {
19
    color: #fff;
20
    font-weight: 400;
21
    padding: 60px 0;
22
    background-color: #0082FF;
23
}
24
25
article header {
26
    border-top: 3px solid #777;
27
    padding: 80px 0;
28
}
29
30
.article-content {
31
    font-size: 1em;
32
    font-weight: 100;
33
    line-height: 2.4em;
34
}
35
36
p {
37
    margin: 4em 0;
38
}
39
40
.container {
41
    width: 700px;
42
    margin: 0 auto;
43
}
44
45
46
footer {
47
    text-align: center;
48
    background-color: #666;
49
    color: #fff;
50
    padding: 40px 0;
51
    margin-top: 60px;
52
}
53
54
.read-next {
55
    font-size: 2em;
56
}

Scroll Position Calculation

To calculate our scroll position, we need to understand conceptually what we are tracking. Since JavaScript can track only the top scroll value, we will need to track our scroll value from 0 (at the top, not scrolled) to whatever the final scroll value is. That final scroll value will be equal to the total document length minus the height of the window itself (because the document will scroll until the bottom of the document reaches the bottom of the window).

We will use the following JavaScript to calculate this scroll position.

1
(function(){
2
    var $w = $(window);
3
    var wh = $w.height();
4
    var h = $('body').height();
5
    var sHeight = h - wh;
6
    $w.on('scroll', function(){
7
        var perc = Math.max(0, Math.min(1, $w.scrollTop()/sHeight));
8
    });
9
10
}());

The above code sets the window height and the body height, and when the user scrolls it uses those values to set a perc variable (short for percentage). We also utilize Math.min and Math.max to limit the values to the 0-100 range.

With this percentage calculation, we can drive the progress indicator.

Circle Indicator

The first indicator we will create is an SVG circle. We will utilize the SVG stroke-dasharray andstroke-dashoffset properties to show progress. First, let's add the progress indicator to the document.

1
<div class="progress-indicator">
2
    <svg>
3
        <g>
4
            <circle cx="0" cy="0" r="20" class="animated-circle" transform="translate(50,50) rotate(-90)"  />
5
        </g>
6
        <g>
7
            <circle cx="0" cy="0" r="38" transform="translate(50,50)"  />
8
        </g>
9
    </svg>
10
    <div class="progress-count"></div>
11
</div>

This markup gives us two circles in an SVG, as well as a containing div to show our percentage count. We need to add style to these elements as well, and then we'll explain how these circles are positioned and animated.

1
.progress-indicator {
2
    position: fixed;
3
    top: 30px;
4
    right: 30px;
5
    width: 100px;
6
    height: 100px;
7
}
8
.progress-count {
9
    position: absolute;
10
    top: 0;
11
    left: 0;
12
    width: 100%;
13
    height: 100%;
14
    text-align: center;
15
    line-height: 100px;
16
    color: #0082FF;
17
}
18
19
svg {
20
    position: absolute;
21
}
22
circle {
23
    fill: rgba(255,255,255,0.9);
24
}
25
26
svg .animated-circle {
27
    fill: transparent;
28
    stroke-width: 40px;
29
    stroke: #0A74DA;
30
    stroke-dasharray: 126;
31
    stroke-dashoffset: 126;
32
}

These styles set us up to animate our circle element. Our progress should always be visible, so we set position to fixed on the .progress-indicator class, with positioning and sizing rules. We also set our progress count to be centered both vertically and horizontally inside this div.

The circles are positioned in the center using transform on the SVG elements themselves. We start the center of our circles using transform. We use a technique here that allows us to apply a rotation from the center of our circles in order to start the animation at the top of the circle (rather than the right side of the circle). In SVG, transforms are applied from the top left of an element. This is why we must center our circles at 0, 0, and move the circle's center to the center of the SVG itself using translate(50, 50).

Using stroke-dasharray and stroke-dashoffset

The properties stroke-dasharray andstroke-dashoffset allow us to animate the stroke of an SVG. stroke-dasharray defines the visible pieces of a stroke.  stroke-dashoffset moves the start of the stroke. These attributes combined allow us to create a stroke "keyframing" process.

Updating stroke-dasharray on Scroll

Next, we will add a function to update the stroke-dasharray on scroll, using our percentage progress previously shown.

1
(function(){
2
    var $w = $(window);
3
    var $circ = $('.animated-circle');
4
    var $progCount = $('.progress-count');
5
    var wh = $w.height();
6
    var h = $('body').height();
7
    var sHeight = h - wh;
8
    $w.on('scroll', function(){
9
        var perc = Math.max(0, Math.min(1, $w.scrollTop()/sHeight));
10
        updateProgress(perc);
11
    });
12
13
    function updateProgress(perc){
14
        var circle_offset = 126 * perc;
15
        $circ.css({
16
            "stroke-dashoffset" : 126 - circle_offset
17
        });
18
        $progCount.html(Math.round(perc * 100) + "%");
19
    }
20
21
}());

The offset that matches our circle happens to be about 126. It's important to note that this won't work for all circles, as 126 is about the circumference of a circle with a radius of 20. To calculate the stroke-dashoffset for a given circle, mutiply the radius by 2PI. In our case, the exact offset would be 20 * 2PI = 125.66370614359172.

Horizontal Progress Variation

For our next example, we'll make a simple horizontal bar fixed to the top of the window. To accomplish this, we'll use an empty progress indicator div.

1
<div class="progress-indicator-2"></div>

Note: we've added the "-2" to allow us to include this example in the same CSS file.

Next, we'll add our styling for this element.

1
.progress-indicator-2 {
2
    position: fixed;
3
    top: 0;
4
    left: 0;
5
    height: 3px;
6
    background-color: #0A74DA;
7
}

Finally, we will set the width of the progress bar on scroll.

1
var $prog2 = $('.progress-indicator-2');
2
function updateProgress(perc){
3
    $prog2.css({width : perc*100 + '%'});
4
}

All together, our final JavaScript should look like this:

1
(function(){
2
    var $w = $(window);
3
    var $circ = $('.animated-circle');
4
    var $progCount = $('.progress-count');
5
    var $prog2 = $('.progress-indicator-2');
6
    var wh = $w.height();
7
    var h = $('body').height();
8
    var sHeight = h - wh;
9
    $w.on('scroll', function(){
10
        var perc = Math.max(0, Math.min(1, $w.scrollTop()/sHeight));
11
        updateProgress(perc);
12
    });
13
14
    function updateProgress(perc){
15
        var circle_offset = 126 * perc;
16
        $circ.css({
17
            "stroke-dashoffset" : 126 - circle_offset
18
        });
19
        $progCount.html(Math.round(perc * 100) + "%");
20
21
        $prog2.css({width : perc*100 + '%'});
22
    }
23
24
}());

Other Ideas for Progress Bars

This article is intended to give you the tools and inspiration to design your own scroll progress solutions. Other ideas for progress bars might include using more descriptive or humanized terms for the progress indication itself, such as "halfway there" or "just getting started". Some implementations (like the ia.net example shown previously) use estimation of an article's read time. This could be estimated using code similar to the following:

1
var wordsPerMin = 300; // based on this article: http://www.forbes.com/sites/brettnelson/2012/06/04/do-you-read-fast-enough-to-be-successful/

2
var wordsArray = $(".article-content").text().split(" ");
3
var wordCount = wordsArray.length;
4
var minCount = Math.round(wordCount / wordsPerMin);

You would then use the minCount in conjunction with the perc variable we are updating on scroll to show the reader their remaining time to read the article. Here's a very basic implementation of this concept.

1
function updateProgress(perc){
2
    var minutesCompleted = Math.round(perc * minCount);
3
    var remaining = minCount - minutesCompleted;
4
    if (remaining){
5
        $(".progress-indicator").show().html(remaining + " minutes remaining");
6
    } else {
7
        $(".progress-indicator").hide();
8
    }
9
}

One Final Piece: Adaptive Screen Sizing

To ensure that our progress indicator works as it should, we should make sure our math is calculating the right things at the right times. For that to happen, we need to make sure we are re-calculating the heights and updating the progress indicator when the user resizes the window. Here's an adaptation of the JavaScript to make that happen:

1
(function(){
2
    var $w = $(window);
3
	var $circ = $('.animated-circle');
4
	var $progCount = $('.progress-count');
5
	var $prog2 = $('.progress-indicator-2');
6
7
	var wh, h, sHeight;
8
9
	function setSizes(){
10
		wh = $w.height();
11
		h = $('body').height();
12
		sHeight = h - wh;
13
	}
14
15
	setSizes();
16
17
	$w.on('scroll', function(){
18
		var perc = Math.max(0, Math.min(1, $w.scrollTop()/sHeight));
19
		updateProgress(perc);
20
	}).on('resize', function(){
21
		setSizes();
22
		$w.trigger('scroll');
23
	});
24
25
	function updateProgress(perc){
26
		var circle_offset = 126 * perc;
27
		$circ.css({
28
			"stroke-dashoffset" : 126 - circle_offset
29
		});
30
		$progCount.html(Math.round(perc * 100) + "%");
31
32
		$prog2.css({width : perc*100 + '%'});
33
	}
34
35
}());

This code declares a function which sets the variables we need to calculate the progress at any given screen size, and calls that function on resize. We also re-trigger scroll on window resize so that our updateProgress function is executed.

You've Reached the End!

Having laid the foundation for any number of variants, what can you come up with? What progress indicators have you seen that work? How about indicators that are bad for usability? Share your experiences with us in the comments!

Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Web Design tutorials. Never miss out on learning about the next big thing.
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.