You may have noticed those reading progress indicators on top of some pages (mainly news articles and blog posts). They are a bar that grows as you read the article and lets you know how much you have left to read visually. This component is a nice feature, especially on mobile, where the scrollbars are not always visible. But did you know that you can code a reading indicator with just a single HTML element and some CSS? No JavaScript code at all.
In this post, I will explain how to build a reading indicator in CSS. You can check the source code on this CodePen demo:
The Code
First, you will need only one empty HTML element:
<div class="reading-indicator"></div>
This tag goes at the page's root (using absolute positioning to get the document's height), and then we will use the ::before and ::after pseudo-elements to “draw” the progress bar.
Note: We could create a more straightforward reading progress indicator without an explicit element, directly in the <body> with its ::before and ::after... but, call me old-fashioned, I prefer not to have “unexpected side-effects,” so I like to create an element for it.
The style for the reading-indicator class will be as follows:
.reading-progress { position: absolute; top: 0; left: 0; width: 100%; height: calc(100% - 100vh); z-index: -1; overflow: hidden; }
A couple of important things related to this code:
- The height is a funky value; why is that? Why not just 100%? Using 100% would take the page's full size, making the reading indicator stop progressing before reaching 100% (because the 100% would be the exact bottom of the page). We need to consider the height of the view frame (100vh) and refactor it into the element's height.
-
overflow: hiddenwould not be required, but as you may have noticed, the indicator edge is diagonal instead of vertical, so after reaching 100%, the whole bar wouldn't take the full width. To fix that, I make the pseudo-elements a bit wider than their container and use the overflow: hidden to avoid unnecessary horizontal scroll bars.
This element is only the container. What draws the reading indicator is the pseudo-elements: with ::before, we will draw a horizontal bar that occupies the whole width and has a height and sticks to the top of the page:
.reading-progress::before { content: ""; position: fixed; top: 0; left: -0.5%; width: 101%; height: 10px; background: linear-gradient(90deg, #369, #396); }
But with this, we just have a horizontal line. So how do we make it grow and shrink as the user scrolls around? That's where the trickery begins. We use the ::after pseudo-element that occupies the whole size of the parent and a diagonal background, one half the color of the background (notice this as it will be a problem later) and the other half transparent:
.reading-progress::after { content: ""; position: absolute; top: 0; left: -0.5%; width: 101%; height: 100%; background: linear-gradient(to bottom left, #fff 50%, #fff0 50%); }
And one last (but crucial) style change: we need to make the html tag have a relative position, so its height impacts the reading indicator.
html { position: relative; }
That's it. That's the trick: the ::after pseudo-element covers completely the ::before, but as we scroll on the page and progress on the triangular background, we reveal more and more of the bar (remember it has a fixed position so that one is always visible).
Some Thoughts
This way of creating a reading indicator is not perfect; it has some interesting points and also some drawbacks. Here is a short list of pros and cons:
Pros:
- Simple to code and develop.
- Responsive (relative units FTW!)
- Supported by all browsers.
Cons:
- It requires a flat background.
- Not visually appealing (the end is tilted)
- It only works at the page level
With some tweaking, it may work at the article level. But with the current implementation, it is more of a glorified scrollbar indicator than any other thing. Which doesn't mean it is not practical. As mentioned above, it could be engaging in some scenarios, especially on cell phones or tablets.
I did some unsuccessful tests, but I still think there has to be a different way to do it (maybe using sticky and background-attachment?)