Lately I’ve seen quite a few websites that have some kind of an indicator to display the current reading position (how much you have “read”, depending on how far you have scrolled down an article). Generally, such indicators are used on blog posts or long form articles and help readers understand how far they are from finishing the article.
Here are some examples:
Interestingly, all three techniques represent the same information but with a different approach. I don’t know if there is a name for this feature – so throughout the article, I call it a Reading Position Indicator.
In this article, we’ll focus on the first technique that uses a horizontal progress bar as the indicator. But instead of using traditional div/span(s) and some non-linear math to build the indicator, we will use the HTML5 progress element. In my opinion is much more semantically accurate and suitable to represent this information, and that too with no complex calculations involved.
If you have never used the HTML5 progress element before, then I would strongly recommend you to read my article on CSS-Tricks that gives you an introduction on how to use this element in your markup and style them via CSS as cross-browser as possible with decent fallback techniques
The Problem
To build a reading position indicator, we need to answer two important questions:
- What is the length of the webpage? The length of the webpage is same as the length of the document, which can be calculated via JavaScript.
- What is the current reading position of the user? Determining the current reading position of the user would entail hacking into the user’s mind to extract the portion of the document currently being read by the user. This appears more like a candidate for Artificial Intelligence and seems impossible; given the scope of technologies that we are dealing with.
This leaves us with no choice but to tackle this problem statement with a completely different approach.
Principle
The principle behind this technique is based on a simple fact that the user needs to scroll to reach the end of the web page. Once the user reaches the end of the web page we can conclude that he/she has finished reading the article. Our technique revolves around the scroll event which is likely to be the key to determine an approximate position of the user while reading.
Assuming the user starts reading from the top and will only scroll once he/she reaches the end of the viewport, we’ll attempt to answer the following questions:
- How much the user needs to scroll to reach the end of the web page? The portion of page that is hidden from the viewport is exactly the amount of scroll the user needs to perform to reach the end of the page. This will become our
max
attribute. - How much portion of the page, user has already scrolled? This can be determined by calculating the vertical offset of the top of the document from the top of the window which will become our
value
attribute.
In the context of the browser, document
and window
are two different objects. window
is the viewable area of the browser (thick blue box in the above example) and document is actually the page that loads inside the window (thin grey box currently scrolling).
Markup
Let’s start with a basic markup:
<progress value="0"></progress>
It’s important to explicitly specify the value
attribute. Otherwise, our progress bar will be in the indeterminate state. We don’t want to add unnecessary styles in CSS for the indeterminate state. Thus we choose to ignore this state by specifying the value attribute. Initially, the user starts reading from the top, hence, the starting value set in the markup is 0
. The default value of the max
attribute (if unspecified) is 1
.
To determine the correct value for the max
attribute, we need to subtract the window’s height from the height of the document. This can only be done via JavaScript, so we will worry about it at a later stage.
The placement of the markup in the HTML document would heavily depend on the how rest of the elements are placed. Typically, if you have no fixed position containers in your document, then you can place the progress element right on top of all the elements inside the tag.
<body>
<progress value="0"></progress>
<!--------------------------------
Place the rest of your markup here
--------------------------------->
</body>
Styling the indicator
Since, we want our indicator to always sit on top of the web page, even when the user scrolls, we’ll position the progress element as fixed
. Additionally, we would want the background of our indicator to be transparent
so that an empty progress bar doesn’t create a visual hinderance while scrolling through the web page. At the same time this will also help us tackle browsers with JavaScript disabled that we’ll cover later.
progress {
/* Positioning */
position: fixed;
left: 0;
top: 0;
/* Dimensions */
width: 100%;
height: 5px;
/* Reset the appearance */
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
/* Get rid of the default border in Firefox/Opera. */
border: none;
/* Progress bar container for Firefox/IE10+ */
background-color: transparent;
/* Progress bar value for IE10+ */
color: red;
}
For Blink/Webkit/Firefox, we need to use vendor specific pseudo elements to style the value inside the progress bar. This will be used to add color to our indicator.
progress::-webkit-progress-bar {
background-color: transparent;
}
progress::-webkit-progress-value {
background-color: red;
}
progress::-moz-progress-bar {
background-color: red;
}
Interaction
Calculating the width/height of window and document in JavaScript is messy and varies horribly across different breed of browsers. Thankfully, jQuery manages to abstract all the complexities offered by these browsers and provides a much cleaner mechanism to calculate the dimensions of window and document. Hence, for the rest of the article we’ll rely on jQuery to handle all our interactions with the user.
Before, we begin, do not forget to add jQuery library to your document.
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>
We need jQuery to determine the max
and the value
attribute of our progress element.
- max – The
max
value is the portion of the document that lies outside the viewport which can be calculated by subtracting the window’s height from the height of the document.var winHeight = $(window).height(), docHeight = $(document).height(); max = docHeight - winHeight; $("progress").attr('max', max);
- value – Initially,
value
will be zero (already defined in the markup). However, as soon as the user starts scrolling, the vertical offset of the top of the document from the top of the window will increase. If the scrollbar is at the very top, or if the element is not scrollable, the offset will be0
.var value = $(window).scrollTop(); $("progress").attr('value', value);
document
in $(document).height()
, we can use other elements like section
, article
or div
that holds the content of the article to calculate the height and present the user with a much more accurate representation of the reading position indicator. This becomes quite useful, when you have a blog post that is filled with comments and constitutes more than 50% of the actual article.Now, everytime the user scrolls, we need to re-calculate the y-offset from the top of the window and then set it to the value
attribute of the progress element. Note that the max
attribute remains the same and doesn’t change when the user scrolls.
$(document).on('scroll', function() {
value = $(window).scrollTop();
progressBar.attr('value', value);
});
The direction in which the user is scrolling is not important because we always calculate the y-offset from the top of the window.
It’s important that our code executes only then the DOM is loaded, otherwise, premature calculation of window/document’s height could lead to weird and unpredictable results.
$(document).on('ready', function() {
var winHeight = $(window).height(),
docHeight = $(document).height(),
progressBar = $('progress'),
max, value;
/* Set the max scrollable area */
max = docHeight - winHeight;
progressBar.attr('max', max);
$(document).on('scroll', function(){
value = $(window).scrollTop();
progressBar.attr('value', value);
});
});
(Or ensure this code is loaded at the bottom of the page instead of the top, and skip the document ready call.)
Browser compatibility
This is all what we need to build a functional reading position indicator that works equally well in all the browsers that support the HTML5 progress element. However, the support is limited to Firefox 16+, Opera 11+, Chrome, Safari 6+. IE10+ partially supports them. Opera 11 and 12 doesn’t permit changing the progress bar color. Hence, our indicator reflects the default green color.
Variants
There are quite a few variations possible in which we can style the indicator. Especially, the semantic color scheme (fourth variation) is a useful experiment, wherein the indicator changes color based on the proximity of the reading position from the end of the article.
- Flat color scheme (default)
- Single color gradient
- Multi color gradient
- Semantic color scheme
Edge cases
There are few scenarios, where our code can potentially break or present the user with an incorrect indicator. Let’s look at those edge cases:
Document height <= Window height
So far, our code assumes that the height of the document is greater than the window’s height, which may not be the case always. Fortunately, browsers handle this situation very well by returning the height of the window, when the document is visibly shorter than the window. Hence, docHeight
and winHeight
are the same.
max = docHeight - winHeight; // equal to zero.
This is as good as a progress element with both max
and value
attribute as zero.
<progress value="0" max="0"></progress>
Hence, our progress bar would remain empty and since our background is transparent, there will be no indicator on the page. This makes sense because, when the entire page can fit within the viewport there is really no need for an indicator.
Moreover, the scroll event won’t fire at all because the height of the document doesn’t exceed the window height. Hence, without making any modification, our code is robust enough to handle this edge case.
User resizes the window
When the user resizes the window, the height of the window and the document will change. This means that we will have to recalculate the max
and the value
attribute to reflect the correct position of the indicator. We’ll bind the code that calculates the correct position to the resize event handler.
$(window).on('resize', function() {
winHeight = $(window).height(),
docHeight = $(document).height();
max = docHeight - winHeight;
progressBar.attr('max', max);
value = $(window).scrollTop();
progressBar.attr('value', value);
});
Javascript is disabled
When JavaScript is disabled our progress bar would have the default value
as 0 and max
as 1.
<progress value="0" max="1"></progress>
This would mean that the progress bar would remain empty and wouldn’t affect any part the page. This is as good, as a page with no indicator isn’t a big loss to the reader.
Fallback for older browsers
Older browsers that do not support the HTML5 progress element will simply ignore the progress
tag. However, for some devs providing a consistent experience is important. Hence, in the following section, we’ll employ the same fallback technique that was used in my previous article to implement the reading position indicator for oler browsers.
Markup – The idea is to simulate the look and feel of the progress element with div/span(s)
. Modern browsers will render the progress
element and ignore the markup inside it, whereas older browsers that cannot understand the progress
element will ignore it and instead render the markup inside it.
<progress value="0">
<div class="progress-container">
<span class="progress-bar"></span>
</div>
</progress>
Styling – The container will always span across the width of the webpage and the background will stay transparent to handle other edge cases.
.progress-container {
width: 100%;
background-color: transparent;
position: fixed;
top: 0;
left: 0;
height: 5px;
display: block;
}
.progress-bar {
background-color: red;
width: 0%;
display: block;
height: inherit;
}
Interaction – First we need to separate browsers that do not support the progress
element from the browsers that support them. This can be achieved either with native JavaScript or you can use Modernizr to test the feature.
if ('max' in document.createElement('progress')) {
// Progress element is supported
} else {
// Doesn't support the progress element. Put your fallback code here.
}
The inputs still remain the same. But, in addition to determining the value, we need to calculate the width of the .progress-bar
in percentage.
winHeight = $(window).height();
docHeight = $(document).height();
max = docHeight - winHeight;
value = $(window).scrollTop();
width = (value/max) * 100;
width = width + '%';
$('.progress-bar').css({'width': width});
After exploring all the edge cases, we can refactor the code to remove any duplicate statements and make it more DRY-er.
$(document).ready(function() {
var getMax = function(){
return $(document).height() - $(window).height();
}
var getValue = function(){
return $(window).scrollTop();
}
if ('max' in document.createElement('progress')) {
// Browser supports progress element
var progressBar = $('progress');
// Set the Max attr for the first time
progressBar.attr({ max: getMax() });
$(document).on('scroll', function(){
// On scroll only Value attr needs to be calculated
progressBar.attr({ value: getValue() });
});
$(window).resize(function(){
// On resize, both Max/Value attr needs to be calculated
progressBar.attr({ max: getMax(), value: getValue() });
});
} else {
var progressBar = $('.progress-bar'),
max = getMax(),
value, width;
var getWidth = function() {
// Calculate width in percentage
value = getValue();
width = (value/max) * 100;
width = width + '%';
return width;
}
var setWidth = function(){
progressBar.css({ width: getWidth() });
}
$(document).on('scroll', setWidth);
$(window).on('resize', function(){
// Need to reset the Max attr
max = getMax();
setWidth();
});
}
});
Performance
Generally, it is considered a bad practice to attach handlers to the scroll event because the browser attempts to repaint the content that appears every time you scroll. In our case, the DOM structure and the styles applied to them are simple, hence, we wouldn’t observe any lag or noticeable delay while scrolling. However, when we magnify the scale at which this feature can be implemented on websites that employs complex DOM structure with intricate styles, the scroll experience can become janky and the performance may go for a toss.
If scrolling performance is really becoming a big overhead for you to overcome, then you can either choose to get rid of this feature completely or attempt to optimize your code to avoid unnecessary repaints. Couple of useful articles to get you started:
- John Resig on Learning from Twitter.
- Scrolling performance by Paul Lewis.
Ambiguity
I am no UX expert, but in some cases, the position and appearance of our indicator can be ambiguous and potentially confuse the user. Ajax-driven websites like Medium, Youtube etc., use similar kind of a progress bar to indicate the load status of the next page. Chrome for mobile natively uses a blue color progress bar for the webpage loader. Now, if you add the reading position indicator to this frame, I am sure that an average user will have a hard time understanding what the progress bar at the top of the page really means.
You’ll have to make the call for yourself if this is of benefit to use your users or not.
Pros
- Semantically accurate.
- No math or complex computation involved.
- Minimum markup required.
- Seamless fallback for browsers with no support for HTML5 progress element.
- Seamless fallback for browsers with JavaScript disabled.
Cons
- Cross-browser styling is complex.
- Fallback for older browsers relies on traditional
div/span(s)
technique making the entire code bloat. - Scroll hijacking can potentially reduce FPS on webpages with complex DOM structure and intricate styles.
- It conflicts with the progress bar used to indicate web page loading and might confuse users.
This is very cool, and I am glad I was able to learn some new stuff from detailed and well written article.
But aren’t browser scroll-bars natively serve the same purpose as their secondary function? ))
Yup they do :-) However, this article ain’t about “Whether the indicator can replace the scrollbars or not” but more about “Is there a better way to implement this feature in modern browsers if they are needed? “
As mentioned in Mike’s comment below, this has some use due to blog comments occupying a large amount of the screen height, so the scrollbar makes the article look longer than it really is.
Scroll bars give you an indicator as to where you are in the page, but this gives you a way to indicate where you are in the content. Which is useful, as sometimes Im seeing an article, and the 300+ comments on the bottom of the article give me the initial impression Im about to dive into something gargantuan. Aint nobody got time for that. But this gives the reader a more accurate sense of what they are getting into.
FWW, for good measure try adding a script to fallback to a local copy of jQuery in case Google is down:
<script>window.jQuery || document.write(‘<script src=”js/jquery-1.11.0.min.js”><\/script>’)</script>
—
PS. The Markdown didn’t like this code in inline nor block, so I had to add it as plain text.
Great article – thank you!
Discourse has a similar feature. When I view a topic (at least in a desktop browser) I can see a progress tracker on bottom edge of the viewport:
https://meta.discourse.org/t/welcome-to-meta-discourse-org/1
Here’s another example: my blog (Italian articles). http://akow.co/tic-tac-panda
A small progressbar appears at the bottom of the windows when you are inside the main text part of the article and represents the amount of scrolling left to the end of text. Also, as a bonus, the page remembers where you left your reading when you came back to the article.
It’s very simple and fast vanilla js (http://s.akow.co/script.js) and it’s not optimized nor tested for legacy browser as I had no interest on making my blog working on old stuff.
Maybe I’m being really dumb here, and I did only skim-read the article, but…
Isn’t that what the scrollbar does?
Okay, so with a bit more thought I see this has some use due to blog comments occupying a large amount of the screen height, so the scrollbar makes the article look longer than it really is.
A simpler solution would be to show comments on request, which would also let you lazy-load the comments (including avatars) and improve performance.
The “673 words left” version is interesting.
Yup! you are right. There are many ways to handle this scenario and loading the comments via AJAX as soon as the user reaches the end of the web page is one of them.
Hence, this is why we mentioned that the developer needs to make a conscious decision on whether to use this feature on their website or not.
If you’re concerned about firing intricate events too often, wouldn’t using something like setInterval() do the trick? Granted, you won’t have the instant interaction the articles’ method gives you, but at normal reading speeds, combined with a smooth animation, don’t you think updating the progress bar every 500ms or so would be more than enough?
That is exactly what has been suggested by John Resig in his article on Learning from Twitter which is added as a reference at the bottom of this article for possible ways by which you can improve the scroll performance.
underscope has supplied two methods debounce and throttle enchance scrolling performance.
Might be more interesting to render the reading position indicator as a visualized percentage of scroll offset versus actual article length, with the comments and footers and other such things excluded entirely. Otherwise you’re just replicating the browser scrollbar without the any of the actual functionality that a scrollbar provides. Reading position implies position relative to content, not page length.
Well detailed article. I should try this. Thanks for sharing.
I love this idea. I personally don’t care for blogs with comment sections taking up a large part of the page height. (In many cases, comments take up several TIMES the height of the content itself.) I like to know how much reading I’m really getting myself into and the relative scrollbar position doesn’t help in this situation. (If you’re on mobile, you don’t even get a visible scrollbar.)
I, for one, hope this “mini-trend” catches on in some form. I also like the idea of putting benchmarks on the progress bar (like on Hulu videos) that link to all the headers in the post (if that makes sense). Should be easy to whip up w/ some JS.
I’ve seen these becoming more prominent lately. I myself find them visually distracting, especially when they’re in a strong contrast colour, constantly pulling my eyes to the top of the screen. I think the cost outweighs the benefit — for most sites, anyway — and that they become more of a redundant gimmick than a useful feature. I’d opt for word count or est. reading time in verbatim on top of articles instead — more easily accessible to non-techies and far less distracting.
Nice method of calculation the length of the page. But it doesn’t work well when having a lot of images in your content. Images can take a while to load and the height and width of the images can manipulate the height of the content in total. Using a external plugin like imagesLoaded could help you with that, otherwise you could add width and height to all your images.
I’ve actually tried writing something similar to this before, which would collapse all sections of a page except the one being read, and automatically expand them when the headers came into a trigger region of the window viewport. There was no visual indicator, but it’s nice to know now that it was a solid idea. Anyways, thanks for this article, I will certainly take advantage of these tips in the future!
hihihi :D
How to Build a Page Scroll Progress Indicator With jQuery and SVG
The very first picture on left “red color progress bar” – I really thought you were talking about this – http://github.hubspot.com/pace/docs/welcome/ that got me excited but realized it’s different ;) Great article!
The article does talk about the website loading bar which is what pace.js really is in the Ambiguity section. But unfortunately, this is as far it goes in the context of this article.
Thank you for this article!
Another thing: what about the fashion of infinite-scroll?
Using of this kind of indicator becomes useless in this case. Isn’t it?
Interesting thought! But infinite scroll is rarely used in the context of a blog post or long-form articles. Even if it is, who would want to read a never-ending article? :-)
In nutshell, this would be rendered useless but using infinite scroll for articles is not recommended either!
Yes, you’re right. Thank you for your reply.
What about Disqus or asynchronously loaded comments?
Ok ok, I nitpick :p
Have nice sunday.
Don’t make me think.
That’s kind of a commandment in web design. I have to admit I often temper with new ideas just to find out what they’re good for and what they’re made of.
Anyway, I’ll stick to scrollbars and, well, short content. Doesn’t make them think. Users are the cruelest part of our life, but finally it’s them who pay our income. Feed them with what they know. You can try to let it look cool, but is has to taste like the same old boring stuff they were fed when they grew up.
Pretty cool! And very easy to implement too.
I believe Andriod browsers implement this behavior to show page download percentage.
I noticed that browser resize breaks the functionality.
Cool idea. I’ve made a vanilla JavaScript version of it: http://github.com/jeremenichelli/scrollProgress and it’s IE9+.