Scroll Progress Circle — Free SVG Reading Ring JS Snippet

Scroll Progress Circle · Scroll · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

SVG dash ring
One circle with stroke-dasharray fills by scroll fraction.
Starts at top
A -90° rotation begins the fill at twelve o'clock.
Container-scoped
Reads a panel's scroll metrics, works in modals or sidebars.
Clamped ratio
0–1 clamp prevents overscroll from breaking the ring.
rAF throttling
One DOM write per frame for smooth, cheap updates.
Completion state
Green glow and percentage fade at 100%.
Back-to-top
Becomes a smooth-scroll button only when complete.
No dependency
Pure HTML/CSS/JS — no scroll or animation library.

About this UI Snippet

Scroll Progress Circle — SVG Reading Progress Ring

Screenshot of the Scroll Progress Circle snippet rendered live

A scroll progress circle is a compact, fixed ring that fills clockwise as a reader moves through an article — the circular cousin of the thin top progress bar, popular on blogs and long-form pages because it doubles as a back-to-top button when complete. This snippet builds one in HTML, CSS, and vanilla JavaScript using a single SVG circle and the stroke-dasharray technique, scoped to a scrollable panel rather than the whole window. No dependency.

The stroke-dasharray ring

Two concentric <circle>s share a radius: a grey track and a colored foreground. The foreground's stroke-dasharray is set to its full circumference (2πr ≈ 125.66 for r=20), and the stroke-dashoffset starts at that same value so none of the stroke shows. Reducing the offset toward zero reveals the stroke proportionally — set offset = circumference × (1 - ratio) and the ring fills exactly to the scroll fraction. The SVG is rotated -90° so the fill begins at the top, not at three o'clock.

Progress from scroll geometry

The fraction is scrollTop / (scrollHeight - clientHeight) — how far you've scrolled divided by the total scrollable distance — clamped to 0–1 so overscroll bounce can't push it past full or below empty. Because it reads the *panel's* metrics, not the document's, the same code works for a sidebar, a modal, or a full-page reader without changes.

rAF-throttled scroll handling

Scroll events fire rapidly, so the handler is throttled with a ticking flag and requestAnimationFrame: it does the DOM writes at most once per frame and ignores intermediate events. That keeps the ring smooth and avoids layout thrash even during fast flicks on a trackpad or touch screen.

Completion state and back-to-top

When the ratio hits ~100% a .done class turns the stroke green with a glow, fades out the percentage, and reveals an up-arrow — and only then does the ring become clickable, scrolling the panel back to the top with scrollTo({ behavior: 'smooth' }). Gating the click on completion means it can't be triggered accidentally mid-read.

Reusing it on a real page

Point the listener at window and read document.documentElement metrics to track the whole page instead of a panel. The ring element is position: sticky here so it stays visible while scrolling its container; switch to fixed for a page-level badge. Everything else — the dash math, the throttle, the completion logic — stays identical.

Build with AI

Build, Understand, Optimize, and Extend It With AI

You don't have to work out the stroke-dasharray geometry by hand. Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly why the circle needs a -90 degree rotation to start its fill at twelve o'clock, or why the stroke-dashoffset formula uses circumference times (1 - ratio) instead of just the ratio directly. The same assistant can help optimize it — asking whether the rAF-throttled ticking flag is doing enough under very fast trackpad flicks, or whether the 0.999 completion threshold should account for sub-pixel rounding on high-DPI screens. It's also useful for extending the effect: ask it to add a second ring showing time-remaining estimated from reading speed, make the ring's color transition through a gradient as it fills rather than snapping to green only at completion, or scope multiple rings to multiple independent scrollable panels on the same page. Treat the code less like a finished artifact and more like a starting point for a conversation.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a "scroll progress circle" reading indicator in plain HTML, CSS, and JavaScript using only an inline SVG and the native scroll event — no animation library, no canvas.

Requirements:
- A scrollable panel (its own overflow-y: auto container, not the whole window) containing article content, with a sticky circular badge in the corner containing two concentric SVG circles of the same radius: a static grey background track and a colored foreground stroke, plus a centered percentage label.
- Compute the circle's circumference in JavaScript from its radius (2 * Math.PI * r) and set the foreground circle's stroke-dasharray to that value, with stroke-dashoffset initially equal to the same value so no stroke is visible at rest.
- Rotate the whole SVG -90 degrees in CSS so the fill visually begins at the top of the circle rather than at the 3 o'clock position.
- On every scroll event of the panel (not window), compute the scroll ratio as scrollTop divided by (scrollHeight - clientHeight), clamp it strictly between 0 and 1, and set the foreground stroke-dashoffset to circumference times (1 minus that ratio) so the visible stroke length always matches the scrolled fraction.
- Throttle the scroll handler so DOM writes happen at most once per animation frame, using a boolean flag plus requestAnimationFrame rather than a fixed-interval debounce or throttle library.
- Update the centered percentage label's text every time the ratio updates, and once the ratio reaches essentially 100%, toggle a completed state that changes the ring's stroke color, adds a glow, hides the percentage text, and reveals an arrow icon.
- Make the ring clickable only while in the completed state, and clicking it should smooth-scroll the panel back to its top using scrollTo with behavior: smooth.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSA scrollable reader panel renders with a progress ring in the corner.
  2. 2
    Scroll the panelThe ring's stroke sweeps clockwise and the center percentage climbs.
  3. 3
    Watch the geometryProgress equals scrollTop divided by the total scrollable distance.
  4. 4
    Reach the endThe ring turns green, glows, and shows an up-arrow.
  5. 5
    Click to returnOnce complete, clicking smooth-scrolls back to the top.
  6. 6
    Track the whole pagePoint the listener at window for a page-level badge.

Real-world uses

Common Use Cases

Article reading progress
A compact alternative to a scroll progress bar.
Long-form blogs
Reassure readers atop an article card layout.
Docs and guides
Track position beside a table of contents.
Back-to-top control
Replace a plain scroll to top button.
Any ring progress
Reuse the dash math from an SVG progress ring.
Learning SVG strokes
A reference for stroke-dasharray progress animation.

Got questions?

Frequently Asked Questions

The foreground circle's stroke-dasharray is set to its full circumference and its stroke-dashoffset starts at that same value, hiding the stroke. Setting offset = circumference × (1 - ratio) reveals exactly the scrolled fraction of the stroke. The SVG is rotated -90° so the fill starts at the top of the ring.

It is scrollTop divided by (scrollHeight - clientHeight) — distance scrolled over total scrollable distance — clamped between 0 and 1 so overscroll bounce can't exceed full or drop below empty. It reads the panel's own metrics, so the same logic works in a modal, sidebar, or any scroll container.

Scroll events fire many times per frame, and updating the DOM on each one causes jank. A ticking flag plus requestAnimationFrame coalesces them so the offset and percentage are written at most once per frame, keeping the ring smooth during fast scrolling without wasted layout work.

When the ratio reaches about 100%, a .done class turns the stroke green, fades the percentage, and shows an up-arrow. The click handler only scrolls to the top when that class is present, using scrollTo with smooth behavior — so the button can't be triggered before you've finished reading.

Keep a ref to the scroll container, attach the scroll listener in a mount effect (useEffect, onMounted, ngAfterViewInit), and clean it up on unmount. Store the ratio in state and bind strokeDashoffset to circumference × (1 - ratio). For a whole-page version, listen on window and read document.documentElement metrics. Tailwind handles the layout; bind the dash offset inline.