You’ve seen it on Medium, on major blogs, on documentation sites: a thin colored bar at the top of the page that fills up as you scroll down an article. It’s small, it’s subtle, and it dramatically improves the reading experience. In this tutorial, we’ll build a reading progress bar with CSS and vanilla JavaScript that you can drop into any blog, WordPress site, or custom project in under 5 minutes.
No frameworks. No libraries. Just clean code you can copy, paste, and style however you like.
What Is a Reading Progress Bar?
A reading progress bar is a visual indicator, usually fixed at the top of the viewport, that reflects how far a user has scrolled through the main content of a page. It answers a simple but powerful question for your readers: how much is left to read?
Benefits of adding one to your blog:
- Reduces bounce rate on long-form articles
- Sets clear expectations about article length
- Provides a subtle, modern UX touch
- Encourages users to keep scrolling

Two Approaches: CSS-Only vs. JavaScript
Before jumping into code, let’s compare the two main approaches so you can choose the right one for your project.
| Feature | CSS-Only (animation-timeline) | JavaScript |
|---|---|---|
| Browser support | Modern browsers only | Universal |
| Performance | Excellent (native) | Very good with throttling |
| Flexibility | Limited | Full control |
| Track only article content | Yes | Yes |
We’ll cover both. Start with the JavaScript version if you need broad support, or jump to the CSS-only version if you’re targeting modern browsers.
Step 1: Add the HTML Markup
The bar itself is just an empty div placed near the top of your body. Add this snippet right after your opening <body> tag:
<div id="reading-progress"></div>
That’s it. The div will be styled and updated dynamically.
Step 2: Style the Bar With CSS
Here is the base CSS. It fixes the bar at the top, gives it height, and prepares the fill state:
#reading-progress {
position: fixed;
top: 0;
left: 0;
height: 4px;
width: 0%;
background: linear-gradient(90deg, #ff6b6b, #f06595);
z-index: 9999;
transition: width 0.1s ease-out;
}
@media (max-width: 600px) {
#reading-progress {
height: 3px;
}
}
Key styling choices explained:
- position: fixed keeps the bar visible while scrolling
- z-index: 9999 ensures it stays above sticky headers and modals
- transition smooths out the fill animation
- Media query reduces height on mobile for a lighter feel

Step 3: The JavaScript Logic
This is where the magic happens. Add this script before the closing </body> tag:
<script>
(function() {
const bar = document.getElementById('reading-progress');
const article = document.querySelector('article') || document.body;
function updateProgress() {
const rect = article.getBoundingClientRect();
const total = rect.height - window.innerHeight;
const scrolled = -rect.top;
const percent = Math.min(Math.max((scrolled / total) * 100, 0), 100);
bar.style.width = percent + '%';
}
let ticking = false;
window.addEventListener('scroll', function() {
if (!ticking) {
window.requestAnimationFrame(function() {
updateProgress();
ticking = false;
});
ticking = true;
}
});
updateProgress();
})();
</script>
What the script does:
- Selects the progress bar and the article container
- Calculates the total scrollable distance within the article
- Determines how much of that distance has been scrolled
- Updates the bar width using
requestAnimationFramefor smooth performance
Note: the script targets the <article> element. This means the bar only fills while the user is inside the article, not while scrolling past headers or footers. If your theme doesn’t use an <article> tag, replace the selector with your content wrapper class, for example .entry-content or .post-content.
Step 4: The CSS-Only Alternative
If you only need to support modern browsers, you can skip JavaScript entirely using animation-timeline: scroll(). Here is the complete snippet:
#reading-progress {
position: fixed;
top: 0;
left: 0;
height: 4px;
width: 100%;
background: linear-gradient(90deg, #ff6b6b, #f06595);
transform-origin: 0 50%;
animation: progress linear;
animation-timeline: scroll(root);
z-index: 9999;
}
@keyframes progress {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}
This uses the scroll-driven animations feature, which is supported in Chrome, Edge, and Opera. Firefox and Safari support is progressing, so treat this as progressive enhancement.
How to Add It to WordPress
There are three easy ways to integrate the code into a WordPress site.
Option 1: Via a Child Theme
- Add the CSS to your child theme’s
style.css - Add the JavaScript to a new file called
progress.jsin your child theme - Enqueue both in
functions.phpusingwp_enqueue_styleandwp_enqueue_script - Insert the
<div id="reading-progress"></div>in yourheader.php
Option 2: Using a Code Snippet Plugin
Plugins like WPCode or Code Snippets let you inject the HTML div, CSS, and JS without editing theme files. Set the snippet to run only on single blog posts using their conditional logic.
Option 3: Directly in the Block Editor
Use a Custom HTML block at the top of your post template to insert the markup, then add the CSS via Appearance > Customize > Additional CSS.

Mobile Behavior and Best Practices
Mobile users represent the majority of blog traffic, so the bar should feel native on small screens. A few tips:
- Keep it thin. 3px is enough on mobile. Anything taller competes with your content.
- Avoid distracting animations. A simple width transition of 0.1s is enough.
- Respect reduced motion. Wrap animations in a
@media (prefers-reduced-motion: no-preference)query. - Test with sticky headers. If your header is sticky, make sure the bar sits above it or just below it, not overlapping the logo.
- Use accessible colors. The bar should have sufficient contrast against your header background.
Styling Variations
The base version uses a gradient, but you can go creative:
- Solid brand color for a minimal look
- Animated shimmer using a repeating linear gradient
- Bottom placement by changing
top: 0tobottom: 0 - Vertical bar on the right edge, useful for documentation sites
Debugging Common Issues
| Problem | Likely Cause |
|---|---|
| Bar doesn’t move | Selector doesn’t match your content container |
| Bar fills too early | Article element is shorter than the viewport |
| Bar hidden behind header | Header has higher z-index than 9999 |
| Choppy animation | Scroll handler is not throttled with requestAnimationFrame |
FAQ
Does a reading progress bar affect page speed?
No. The script is a few lines and uses requestAnimationFrame, which piggybacks on the browser’s paint cycle. Impact on Core Web Vitals is negligible.
Can I use this on non-blog pages?
Yes, but it’s best suited for long-form content. On short landing pages or product pages, the bar can feel out of place.
Should I use the CSS-only version in production?
If your audience uses modern Chromium browsers, yes. Otherwise, use the JavaScript version or combine both with a feature detection fallback.
How do I hide the bar when the article is fully read?
In the JavaScript version, add a check: bar.style.opacity = percent >= 100 ? '0' : '1'; along with a CSS transition on opacity.
Does the bar work with infinite scroll blogs?
Not out of the box. Infinite scroll makes the total scroll height dynamic, so you’ll need to recalculate on each new post load, or scope the bar to the currently visible article using IntersectionObserver.
Wrapping Up
A reading progress bar is one of those tiny UX improvements that punches above its weight. In under 30 lines of code, you give readers a clearer sense of pacing and a more polished experience. Copy the snippets above, tweak the colors to match your brand, and ship it today.
