18 CSS Scroll Progress Bar

18 hand-coded CSS scroll progress bar patterns for blog article readers, SaaS onboarding wizards, e-commerce checkout flows, documentation sites, and long-form landing pages. Covers top fixed reading bar, sticky glassmorphic header, radial SVG ring, percentage tooltip, back-to-top circular morph, multi-section TOC scrollspy, checkout step progress, and signup form scroll with save-and-restore. All 18 use animation-timeline: scroll() where supported with rAF-throttled scroll-listener fallback for Safari + Firefox. Scoped under .sp-NN for no-collision pasting, prefers-reduced-motion guarded, framework-agnostic.

18 light JSPublished

Related11 CSS animation-timeline Demos25 CSS Number Counter Animations25 CSS Sticky Navigation20 CSS Scroll Animations12 CSS Full Page Sections

Top Fixed Reading Progress Bar (Pure CSS, animation-timeline: scroll()) — preview
01 / 18Light JS

Top Fixed Reading Progress Bar (Pure CSS, animation-timeline: scroll())

The canonical reading-progress bar, rebuilt with zero JavaScript on the hot path. One @keyframes block plus animation-timeline: scroll() hands the whole thing to the compositor, so a 6,000-word article scrolls without a single scroll event, a single layout read, or a single frame of jank — and a five-line script keeps it working in browsers that have not shipped scroll timelines yet.

Published

Sticky Header with Integrated Scroll Progress Bar (Glassmorphic) — preview
02 / 18Light JS

Sticky Header with Integrated Scroll Progress Bar (Glassmorphic)

The pattern most publishers actually ship: the progress bar is not a separate strip floating over the page, it is the bottom edge of a frosted-glass header. Content dissolves under the blur as it passes, the hairline fills left-to-right, and the header quietly gains depth the moment it detaches from the top of the page.

Published

Floating Radial SVG Progress Ring (Bottom-Right) — preview
03 / 18Light JS

Floating Radial SVG Progress Ring (Bottom-Right)

A 56px ring that hovers in the bottom-right corner and closes as the reader descends — the indicator of choice when the top of the page is already crowded with a header, a breadcrumb and an announcement bar. Built on the pathLength="1" trick so the maths is literally 0→1, with a conic-gradient variant for teams who would rather not ship an SVG at all.

Published

Scroll Progress Bar with Percentage Counter Tooltip — preview
04 / 18Light JS

Scroll Progress Bar with Percentage Counter Tooltip

A progress bar with a live numeric readout that rides along the fill — and the number itself is generated by CSS, not JavaScript. Registering an <integer> custom property with @property makes it animatable, and counter() turns that animated integer into rendered text. The result is a scroll percentage with zero string concatenation on any frame.

Published

Scroll Progress Bar with Back-to-Top Button (Circular Morph) — preview
05 / 18Light JS

Scroll Progress Bar with Back-to-Top Button (Circular Morph)

The back-to-top control, upgraded: it does not just appear, it reports. The ring around it is the page progress, it only materialises once the reader is genuinely committed, and on hover it morphs from a 52px circle into a pill with a label — one element, one grid-template-columns transition, no JavaScript for any of the motion.

Published

Gradient Animated Multi-Color Scroll Bar — preview
06 / 18Light JS

Gradient Animated Multi-Color Scroll Bar

A progress bar where the colour itself travels. The fill scales as usual, but a 300%-wide multi-stop gradient slides through it as you scroll, so the leading edge is a different hue at 20% than at 80% — and because the stops are interpolated in oklch, there are none of the muddy grey midpoints that sRGB gradients produce between complementary colours.

Published

Vertical Side-Docked Scroll Progress Line — preview
07 / 18Light JS

Vertical Side-Docked Scroll Progress Line

A hairline rail docked to the side of the viewport, filling downward as the page moves — the indicator for designs where the top edge is sacred. Section dots along the rail light up as their section enters the viewport, using view() timelines rather than an observer, so the whole thing is a single element plus one dot per section.

Published

Container-Scoped Scroll Progress Bar (Inside a Div) — preview
08 / 18Light JS

Container-Scoped Scroll Progress Bar (Inside a Div)

The version nobody documents: the thing that scrolls is not the page, it is a panel. App shells, modals, chat logs, terms-and-conditions boxes and dashboard cards all scroll their own overflow container — where scroll(root) is permanently zero. Named scroll timelines plus timeline-scope are the correct answer, and this demo is the complete recipe.

Published

Multi-Section TOC Scroll Progress Tracker — preview
09 / 18Light JS

Multi-Section TOC Scroll Progress Tracker

The documentation sidebar, upgraded from a binary highlight to a real per-section meter. Each table-of-contents entry has its own progress line that fills as its section passes through the viewport — so you can see not just which chapter you are in, but how far through it you are. Scrollspy without a single observer.

Published

Step-Segmented Scroll Progress Bar (Section-by-Section) — preview
10 / 18Light JS

Step-Segmented Scroll Progress Bar (Section-by-Section)

Instagram-stories pacing applied to long-form content: one segment per chapter, each filling independently as its own section scrolls past. It answers a question a single continuous bar cannot — how much of this chapter is left — which is why long documentation and multi-part guides convert better with it.

Published

Horizontal Carousel Scroll Progress Bar — preview
11 / 18Light JS

Horizontal Carousel Scroll Progress Bar

Native scroll-snap carousels are the right way to build a slider in 2026 — no library, no transform maths, real momentum on touch. The one thing they lack is a sense of position, and that is exactly what an inline-axis scroll timeline restores: a progress bar underneath the track that is impossible to desynchronise, because it is the scroll position.

Published

E-Commerce Checkout Step Progress on Scroll — preview
12 / 18Light JS

E-Commerce Checkout Step Progress on Scroll

Single-page checkouts convert better than multi-page wizards, but they lose the wizard's most valuable feature: a visible sense of how much is left. Binding a classic four-step stepper to scroll position restores it. Each step activates as its panel reaches the viewport, the connecting line fills continuously, and nothing about the form state is involved.

Published

Signup / Onboarding Form Scroll Progress with Save-and-Restore — preview
13 / 18Light JS

Signup / Onboarding Form Scroll Progress with Save-and-Restore

Long onboarding forms lose people. This pattern fights both causes at once: a two-track indicator that separates how far down the page you are from how many answers you have actually given, and a draft that quietly saves every answer plus your scroll position so a closed tab is never a lost signup.

Published

Tailwind CSS Scroll Progress Bar (@theme + arbitrary values) — preview
14 / 18Light JS

Tailwind CSS Scroll Progress Bar (@theme + arbitrary values)

Tailwind has no built-in utility for scroll-driven animations, which sends most people back to a <style> tag. It does not have to: v4's @theme and @utility directives express the whole pattern as first-class tokens, and arbitrary properties cover the rest inline. This demo ships both — the token-based utility and the pure-inline version — with the exact underscore syntax that trips everyone up.

Published

Custom ::-webkit-scrollbar with Progress Indicator — preview
15 / 18Light JS

Custom ::-webkit-scrollbar with Progress Indicator

Two scrollbar systems exist and neither is going away: WebKit's shadow pseudo-elements and the standard scrollbar-width/scrollbar-color pair. This demo ships both correctly — then shows why the popular 'fill the scrollbar track with a gradient' trick cannot work (backgrounds stop at the padding box) and replaces it with a six-pixel gauge that reads the panel's own scroll timeline.

Published

Rainbow Hue-Rotating Scroll Progress Bar — preview
16 / 18Light JS

Rainbow Hue-Rotating Scroll Progress Bar

The bar starts red and walks the entire colour wheel by the time you reach the footer — so the hue itself encodes position. Two implementations sit side by side: a registered <angle> property fed straight into oklch() (precise, perceptually even, controllable) and a one-line filter: hue-rotate() (cheap, blunt, works on anything).

Published

Cyberpunk / Neon Glow Dark-Mode Progress Bar — preview
17 / 18Light JS

Cyberpunk / Neon Glow Dark-Mode Progress Bar

A neon tube rather than a rectangle: three stacked shadows produce a real bloom, a brighter nib burns at the leading edge, and a faint scanline layer sits over the whole page. It is the maximalist end of this collection — and it is built from four declarations, not a canvas.

Published

Ultra-Thin 2px Minimalist Micro Progress Line — preview
18 / 18Light JS

Ultra-Thin 2px Minimalist Micro Progress Line

The version to ship when the design should not be noticed: two pixels, no track, no glow, no percentage. It stays invisible over the masthead and only materialises once the reader is inside the article — the most restrained pattern in this collection and, for text-first publications, usually the correct one.

Published

FAQ

Frequently asked questions

What's the modern CSS way to build a scroll progress bar without a scroll listener?
The modern recipe uses animation-timeline: scroll(), a CSS-only primitive that binds keyframe animations to scroll position rather than wall-clock time. The full recipe is three declarations: .progress-bar { transform-origin: left center; animation: fill linear both; animation-timeline: scroll(root block); } plus @keyframes fill { from { transform: scaleX(0) } to { transform: scaleX(1) } }. The animation is entirely off the main thread, so the progress bar cannot judder even when JavaScript is busy — a real problem with the classic scroll-event listener pattern that this replaces. Demo 01 (Top Fixed Reading Progress Bar) is the reference implementation. The catch: as of 2026, animation-timeline is Chromium-only (Chrome 115+ / Edge 115+ / Opera 101+); Safari and Firefox haven't shipped it yet. Every demo in this collection ships a JavaScript fallback for those browsers — a 30-line rAF-throttled scroll listener guarded behind CSS.supports('animation-timeline', 'scroll()') so modern engines download the fallback but never run it. Progressive enhancement without the fragility.
How do I add a scroll progress bar with a percentage counter that stays in sync?
The trick is deriving both values from a single source of truth. Demo 04 (Scroll Progress Bar with Percentage Counter Tooltip) shows the pattern: write ONE custom property to the root on each scroll frame, then let CSS derive everything else from it. In JavaScript: const p = Math.min(1, Math.max(0, window.scrollY / (document.documentElement.scrollHeight - innerHeight))); document.documentElement.style.setProperty('--sp-04-p', p.toFixed(4)); document.documentElement.style.setProperty('--sp-04-n', Math.round(p * 100));. In CSS: bar width is scaleX(var(--sp-04-p)); percentage counter is @property --sp-04-n { syntax: '<integer>'; inherits: true; initial-value: 0 } registered at the top of the stylesheet so counter() can render it as text: .pct { counter-reset: n var(--sp-04-n) } .pct::after { content: counter(n) '%' }. Result: bar fill + percentage counter can never desync internally because they read the same source. Wrap the listener in requestAnimationFrame throttling so it fires at most once per frame, and { passive: true } so touch inertia never jitters.
How do I build a circular SVG scroll progress ring that also functions as a back-to-top button?
Demo 05 (Scroll Progress Bar with Back-to-Top Button, Circular Morph) is the reference. The recipe: an SVG <circle> with stroke-dasharray set to its circumference (2π × r, so for r=22 that's 138.23). Animate stroke-dashoffset from full circumference down to 0 as scroll progress increases: stroke-dashoffset: calc(var(--sp-05-c) * (1 - var(--sp-05-p, 0))). The ring visually fills clockwise. Wrap the whole SVG in an <a href="#top"> — the ring becomes clickable at any progress state, and hitting the top smooth-scrolls back. Two production refinements: (1) hide the button when scroll progress is under 10% via opacity: 0; pointer-events: none so it doesn't clutter the initial view; (2) add scroll-behavior: smooth to html BUT wrap it in @media (prefers-reduced-motion: no-preference) so users who requested less motion get an instant jump. The pattern Vercel, Linear, Medium, Substack, and Ghost all ship.
How do I gate a scroll progress bar to a specific container instead of the whole page?
Demo 08 (Container-Scoped Scroll Progress Bar Inside a Div) shows this pattern — critical for modal readers, in-page article panels, embedded docs, or scrollable code blocks. The animation-timeline: scroll() function accepts a scoping argument: scroll(nearest block) walks UP the DOM from the animated element looking for the closest ancestor that has overflow: auto / scroll and binds the timeline to that container's scroll offset. This is powerful but fragile — a stray overflow: hidden ancestor (rounded-corner clipping, for instance) becomes the target and the timeline attaches to a box that never scrolls, leaving the bar frozen at zero with no error. The safer explicit alternative: scroll-timeline: --sp-08-t block declared on the scroller you want (creates a named timeline), then animation-timeline: --sp-08-t on the progress bar (subscribes to it). Named timelines only match elements they share a parent chain with, so the same DOM-walking issue applies but you can debug it. For a fully cross-browser approach, use the JavaScript pattern: containerRef.addEventListener('scroll', () => { const p = containerRef.scrollTop / (containerRef.scrollHeight - containerRef.clientHeight); ...; }, { passive: true });. Same math, works everywhere.
How do I build a multi-section table-of-contents scroll progress tracker (Docusaurus / Mintlify pattern)?
Demo 09 (Multi-Section TOC Scroll Progress Tracker) is the reference. Two moving parts: (1) a sticky sidebar TOC that highlights the currently-visible section as the reader scrolls, and (2) a per-section progress indicator (small filled dot or bar) showing how much of each section has been read. Use IntersectionObserver with rootMargin: '-80px 0px -60% 0px' — top negative offset accounts for the sticky header height; bottom -60% means a section only counts as "active" when its top edge crosses into the top 40% of the viewport. On isIntersecting, add aria-current="location" to the corresponding TOC link (semantic — screen readers announce it as "current location"; NOT aria-current="page" which means "this is the current page in a navigation set"). Style off the ARIA attribute in CSS: a[aria-current="location"] { color: var(--accent); border-inline-start-color: currentColor }. For the per-section progress inside each TOC dot: track section scroll depth with a second observer that measures entry.intersectionRatio and writes it to a custom property on the dot. All 22 lines of JavaScript. Docusaurus, Mintlify, Nextra, VitePress, Astro Starlight, and MkDocs Material all ship this pattern.
How do I preserve a signup form's scroll progress + field values across page reloads?
Demo 13 (Signup / Onboarding Form Scroll Progress with Save-and-Restore) covers the SaaS onboarding pattern. Three save events, all rAF-throttled: (1) on scroll, save the container's scrollTop to localStorage.setItem('form-scroll', String(el.scrollTop)); (2) on every field input event, save the field's value keyed by name; (3) on beforeunload, flush pending saves. On page load, read all keys and restore — el.scrollTop = Number(localStorage.getItem('form-scroll')) for the container plus field.value = localStorage.getItem(field.name) for each field. Add a "Clear saved progress" button that calls localStorage.removeItem for all keys — required by GDPR / CCPA for users who don't want their in-progress data cached. Two important guardrails: (1) namespace localStorage keys by form ID or route (form-onboarding-scroll, not form-scroll) so multi-form pages don't collide; (2) never save password or credit card fields (violates PCI DSS + basic security hygiene — check input.type !== 'password' && input.autocomplete !== 'cc-number' before saving). Result: users who bounce mid-form return to the exact scroll position with fields prefilled — Stripe Atlas, Notion signup, Linear onboarding, and Vercel deploy-a-project all ship this.
Are these scroll progress bars accessible and Core Web Vitals safe?
Yes. Every demo honors prefers-reduced-motion: when the user has requested less motion, the smooth-scroll behavior falls back to instant jumps, the ambient hue-rotate / gradient-drift animations suppress, and the progress bar updates without CSS transitions. Every interactive control (back-to-top button, TOC link, form field) has a visible :focus-visible ring at 2px minimum thickness and 3:1 contrast per WCAG 2.4.13. Progress bars that duplicate the browser's own scrollbar carry aria-hidden="true" because they're decorative — announcing a scroll percentage on every scroll event to screen readers would be noise. Progress bars that carry information the scrollbar doesn't (percentage remaining, section-of-N-sections position) use role="progressbar" + aria-valuenow + aria-valuemin="0" + aria-valuemax="100" and update aria-valuenow from the scroll listener. Core Web Vitals: all 18 use compositor-only CSS properties (transform: scaleX(), not width: X%) for the fill animation, so no layout thrash. requestAnimationFrame throttling keeps scroll listeners under one execution per frame — nothing measurable on INP even during rapid trackpad flicks. Zero CLS impact (progress bars are fixed-position elements that reserve zero layout space). Zero LCP impact (they render after the first paint of real content).
How do these scroll progress bars port to React, Vue, Svelte, Next.js, and Tailwind CSS?
The CSS ports unchanged — every .sp-NN selector, every @property registration, every keyframe is framework-agnostic. What changes is where the JavaScript listener lives. React: useEffect(() => { const cleanup = init(); return cleanup; }, []) at the app-shell level so the listener registers once on mount and removes on route change — never re-register on every render. Vue 3: onMounted(() => init()) + onBeforeUnmount(() => cleanup()) for the same lifecycle. Svelte: onMount(() => { init(); return cleanup }). Next.js App Router: mark the scroll-progress component with 'use client' — scroll position is a browser primitive, no SSR value. Astro: use a client:only="react" or client:load directive on the component. Tailwind CSS: the pattern is mechanical translation — fixed top-0 h-1 w-full bg-transparent for the track, h-full origin-left scale-x-0 for the fill, plus a custom-property token in your @theme block: @theme { --animate-progress: sp-01 linear both; @keyframes sp-01 { from { transform: scaleX(0) } to { transform: scaleX(1) } } }. Demo 14 (Tailwind CSS Scroll Progress Bar) is the reference for Tailwind v4 with @theme + arbitrary values. Every demo's JS is written as a plain init function (15-40 lines) — strip the IIFE wrapper and call from your framework's mount hook. Zero library dependencies.

Related collections

20 CSS Animated Buttons preview

20 CSS Animated Buttons

20 hand-coded CSS animated buttons — neon glow, ripple, 3D press, liquid fill, jelly bounce, shine sweep, animated border, moving gradient CTA, text flip, submit success state, add-to-cart progress, download icon, hamburger-to-close, toggle switch, loading spinner inside button, next/prev arrow nav, and ghost button background reveal. Half pure CSS, half lightweight JS for production interactions.

11 CSS animation-timeline Demos preview

11 CSS animation-timeline Demos

11 pure CSS animation-timeline demos built on the W3C scroll() and view() spec — reading progress bar, view-timeline fade & reveal, shrinking sticky header, stacking cards, horizontal scroll section, parallax hero, container shadow indicators, reverse-scroll columns, text scrim reveal, cover card to sticky header, and image zoom / clip-path wipe. Zero JS, off the main thread, Core Web Vitals safe. Copy-paste ready.

25 CSS Background Animations preview

25 CSS Background Animations

25 hand-coded CSS background animations for SaaS hero sections, developer tool documentation, agency portfolios, gaming and Web3 landing pages, seasonal e-commerce campaigns, and dark-mode dashboards. Covers full-screen gradient shifts, animated mesh gradients, aurora borealis glow, cursor spotlight follow, interactive water ripples, dot grid and diagonal stripe patterns, SVG grain and noise texture overlays, topographic contour lines, layered SVG waves, plasma and lava lamp fluids, bokeh light blur, particle constellation networks, matrix digital rain, synthwave 3D perspective grids, starfield warp speed, CRT scanline overlays, fog and mist drift, morphing organic blobs, floating glassmorphism orbs, halftone dot patterns, starry night skies, floating geometric shapes, rising bubbles, and falling snow. 20 are pure CSS with zero JavaScript; the 5 that need it use vanilla canvas or pointer tracking with no dependencies. Every background animates compositor-only properties so it never blocks the main thread, is scoped under a .bga-NN prefix for no-collision pasting, guards prefers-reduced-motion, and ports unchanged to React, Vue, Svelte, Astro, Next.js and Tailwind.

Search CodeFronts

Loading…