Scroll Progress Bar with HTML, CSS & JavaScript
20 DAYS 20 PROJECT CHALLENGE
Day #16
Project Overview
A lightweight Scroll Progress Bar that displays progress at the top of the page while the user scrolls. It demonstrates listening to scroll events, computing the scrolled fraction (scrollTop / (scrollHeight - clientHeight)), and updating UI smoothly using requestAnimationFrame to avoid jank. This is a commonly used UI pattern for articles, docs, and single-page apps.
Key Features
- Thin progress bar fixed at the top that fills as the user scrolls.
- Smooth, performant updates using
requestAnimationFrame. - CSS variable driven styling (easy to customize colors and height).
- Accessible: progress value available via
aria-valuenowandaria-valuemin/max. - Auto-hide option when at top (optional; included).
- Small, plain JS (no libraries) and responsive.
HTML Code
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Day 16 — Scroll Progress Bar</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<!-- Progress bar (kept outside main content so it's always at top) -->
<div id="scrollProgress" class="scroll-progress" role="progressbar"
aria-label="Page scroll progress" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0">
<div class="scroll-progress__bar" id="scrollProgressBar"></div>
</div>
<main class="page">
<header class="site-header">
<h1>Day 16: Scroll Progress Bar</h1>
<p class="lead">A tiny, performant progress indicator that follows the user's scroll position.</p>
</header>
<article class="content">
<!-- Example long content: copy / paste your own content here -->
<section>
<h2>Introduction</h2>
<p>Scroll down to see the progress bar fill as you move through the page. The progress is calculated as the percent of content scrolled and updated using requestAnimationFrame for smooth animations.</p>
</section>
<!-- repeat filler content to make page long enough to scroll -->
<section>
<h2>Section 1</h2>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer ut ...</p>
<p>(Repeat multiple paragraphs for demo.)</p>
</section>
<section>
<h2>Section 2</h2>
<p>Donec mollis, arcu a convallis efficitur, tortor elit pharetra arcu, quis ...</p>
</section>
<section>
<h2>Section 3</h2>
<p>More content... (Use real article or documentation content in production.)</p>
</section>
<!-- add more content to make the demo scrollable -->
<div style="height:500px;"></div>
<footer style="padding:40px 0 80px; color: #9aa4b2;">
<p>End of demo content.</p>
</footer>
</article>
</main>
<script src="script.js"></script>
</body>
</html>
CSS Code
:root {
--progress-height: 6px;
--progress-bg: rgba(255, 255, 255, 0.04);
--progress-color: linear-gradient(90deg, #7c3aed, #06b6d4);
/* gradient fill */
--progress-transition: width 160ms linear;
--hide-offset: 8px;
/* distance from top to hide when at very top */
}
/* page reset & layout */
* {
box-sizing: border-box
}
html,
body {
height: 100%;
margin: 0;
font-family: Inter, system-ui, -apple-system, 'Segoe UI', Roboto, Arial;
background: #002252;
color: #e6eef6;
-webkit-font-smoothing: antialiased;
}
.page {
max-width: 920px;
margin: 48px auto;
padding: 0 20px;
}
/* progress container - fixed at top */
.scroll-progress {
position: fixed;
top: 0;
left: 0;
right: 0;
height: var(--progress-height);
background: var(--progress-bg);
z-index: 9999;
display: block;
pointer-events: none;
/* avoid intercepting clicks */
transition: transform 280ms ease, opacity 220ms ease;
transform: translateY(0);
opacity: 1;
}
/* hidden state (when at very top) */
.scroll-progress.hidden {
transform: translateY(calc(-1 * (var(--progress-height) + 4px)));
opacity: 0;
}
/* inner fill bar */
.scroll-progress__bar {
height: 100%;
width: 0%;
background: var(--progress-color);
background-clip: padding-box;
/* allow color to be gradient or single color */
transition: var(--progress-transition);
will-change: width;
}
/* header + content styling for demo */
.site-header {
padding: 32px 0 12px;
}
.site-header h1 {
margin: 0 0 6px;
font-size: 22px;
}
.lead {
margin: 0 0 6px;
color: #9aa4b2;
}
/* article content styling */
.content {
line-height: 1.7;
color: #dfe9f5;
}
.content h2 {
margin-top: 28px;
font-size: 18px;
color: #e6eef6;
}
.content p {
margin: 12px 0;
color: #cbd8e6
}
/* small screens tweak */
@media (max-width:520px) {
:root {
--progress-height: 5px;
}
.page {
margin: 28px auto;
padding: 0 14px;
}
} Javascript Code
// Day 16 — Scroll Progress Bar
// Uses requestAnimationFrame for smooth updates and avoids heavy work on each scroll event.
// DOM elements
const progress = document.getElementById('scrollProgress');
const progressBar = document.getElementById('scrollProgressBar');
// Config: auto-hide when at top
const AUTO_HIDE_AT_TOP = true; // set false to always show bar
// internal state
let ticking = false; // rAF flag
function calculateScrollPercent() {
const doc = document.documentElement;
const body = document.body;
// Total scrollable height = document height - viewport height
const scrollTop = window.pageYOffset || doc.scrollTop || body.scrollTop || 0;
const scrollHeight = Math.max(
doc.scrollHeight || 0,
body.scrollHeight || 0
);
const clientHeight = doc.clientHeight || window.innerHeight || 0;
const scrollable = Math.max(0, scrollHeight - clientHeight);
// avoid division by zero on short pages
const fraction = scrollable === 0 ? 0 : (scrollTop / scrollable);
const percent = Math.min(100, Math.max(0, fraction * 100));
return { percent, scrollTop, scrollable };
}
function updateProgress() {
ticking = false;
const { percent, scrollTop } = calculateScrollPercent();
// update bar width and aria attribute
progressBar.style.width = percent + '%';
progress.setAttribute('aria-valuenow', Math.round(percent));
// auto-hide logic (optional): hide when at top (no scroll)
if (AUTO_HIDE_AT_TOP) {
if (scrollTop <= 2) {
progress.classList.add('hidden');
} else {
progress.classList.remove('hidden');
}
}
}
// Scroll event handler — minimal work; schedule rAF if not already scheduled
function onScroll() {
if (!ticking) {
window.requestAnimationFrame(updateProgress);
ticking = true;
}
}
// Also update on resize since viewport height changes scrollable area
function onResize() {
if (!ticking) {
window.requestAnimationFrame(updateProgress);
ticking = true;
}
}
// initialize
function init() {
// initial update (in case page loads scrolled or refreshed)
updateProgress();
// attach listeners
window.addEventListener('scroll', onScroll, { passive: true });
window.addEventListener('resize', onResize);
// also update when content loads images or fonts change layout
window.addEventListener('DOMContentLoaded', updateProgress);
window.addEventListener('load', updateProgress);
}
init();
Subscribe
0 Comments
Oldest
Newest
Most Voted
Inline Feedbacks
View all comments
Related Projects
Day 14 : Image Gallery with Modal
Displays images in a grid with a pop-up modal view.
Concepts: DOM traversal, event bubbling.
Day 18 : Text-to-Speech Converter
Converts entered text into speech.
Concepts: Web Speech API.
Day 19 : Product Filter List
Filter products or items by search or category in real-time.
Concepts: Array filter(), DOM rendering.