Scroll Progress Bar
A thin bar along the top of the screen fills up as the reader scrolls, so they always know how much is left. Track the whole page, one article or a scrolling box.
How to use it
- Paste the snippet just before
</head>. - Add an empty
<div data-scroll-progress-bar></div>anywhere in the page. It fixes itself to the top of the screen.
Options
Add any of these attributes to the same element.
data-target | A selector: measure progress through one element (e.g. #article) or a scrollable box. |
data-color | Any CSS colour or gradient. Default purple gradient. |
data-height | Thickness in px. Default 4. |
data-position="inline" | Stay where you put it instead of fixing to the top of the screen. |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Scroll Progress Bar: a thin bar along the top of the screen fills up as the
// reader scrolls, so they always know how much is left.
// Usage: <div data-scroll-progress-bar></div> (anywhere in the page)
// Optional: data-target="#article" (measure progress through one element, or
// through a scrollable box), data-color (default purple gradient), data-height
// (px, default 4), data-position="inline" (stay where you put it instead of
// fixing to the top of the screen).
var DEFAULTS = { color: 'linear-gradient(90deg, #a855f7, #6a0dad)', height: 4 };
function setup(el) {
var d = el.dataset;
var inline = d.position === 'inline';
el.setAttribute('role', 'progressbar');
el.setAttribute('aria-label', 'Reading progress');
el.setAttribute('aria-valuemin', '0');
el.setAttribute('aria-valuemax', '100');
el.style.cssText += (inline ? 'position:relative;' : 'position:fixed;top:0;left:0;right:0;z-index:2147482000;') +
'height:' + (+d.height || DEFAULTS.height) + 'px;pointer-events:none';
var fill = document.createElement('div');
fill.style.cssText = 'height:100%;background:' + (d.color || DEFAULTS.color) + ';transform-origin:0 50%;transform:scaleX(0);border-radius:0 2px 2px 0';
el.appendChild(fill);
var target = d.target ? document.querySelector(d.target) : null;
var box = null;
if (target) {
var oy = getComputedStyle(target).overflowY;
if (oy === 'auto' || oy === 'scroll') box = target;
}
var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
function progress() {
if (box) return box.scrollTop / Math.max(1, box.scrollHeight - box.clientHeight);
if (target) {
var r = target.getBoundingClientRect();
return -r.top / Math.max(1, r.height - window.innerHeight);
}
var max = document.documentElement.scrollHeight - window.innerHeight;
return window.scrollY / Math.max(1, max);
}
function update() {
var p = Math.min(1, Math.max(0, progress()));
el.setAttribute('aria-valuenow', Math.round(p * 100));
if (reduced) { fill.style.transform = 'scaleX(' + p + ')'; return; }
// A short ease smooths out jumpy wheel scrolling.
anime.animate(fill, { scaleX: p, duration: 250, ease: 'out(3)' });
}
(box || window).addEventListener('scroll', update, { passive: true });
window.addEventListener('resize', update);
update();
}
function init() {
document.querySelectorAll('[data-scroll-progress-bar]').forEach(function (el) {
if (el._spb) return;
el._spb = true;
setup(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-scroll-progress-bar></div>
<!-- progress through the article only: -->
<div data-scroll-progress-bar data-target="#article"></div>