Scroll Progress Footer — Free HTML CSS JS Back-to-Top Ring Snippet

Scroll Progress Footer · Footers · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

SVG stroke-dasharray/stroke-dashoffset ring reused from the Download Button's progress technique
Ring fill tracks real scroll percentage, not a fixed-duration animation
Passive scroll listener with no requestAnimationFrame polling loop
Button fades and slides in only after meaningful scroll depth, not immediately
Native window.scrollTo smooth-scroll on click — no manual scroll animation
prefers-reduced-motion aware — the ring still fills, without the smoothing transition

About this UI Snippet

Scroll Progress Footer — A Back-to-Top Button That Tells You Where You Are

Screenshot of the Scroll Progress Footer snippet rendered live

A plain back-to-top button answers one question — "can I get to the top?" — but says nothing about how far down the page you actually are. This snippet pairs a standard footer with a fixed corner button whose ring fills to the exact percentage of the page you've scrolled, using the same SVG stroke-dash technique as the Download Button's progress ring, driven here by scroll position instead of a timer.

Two concentric circles, one static track and one animated arc

The button is two SVG <circle> elements sharing the same centre and radius: .spf-track is a faint, always-full ring that shows the button's total circumference, and .spf-prog sits on top with stroke-dasharray set to the circle's exact circumference (2πr, computed once in JS as CIRC) and stroke-dashoffset animated toward zero as the arc "draws in." Rotating the whole SVG -90deg makes the arc start filling from 12 o'clock rather than 3 o'clock, the same convention used in most native progress rings.

Scroll position, converted to a fraction, converted to an offset

On every scroll event, onScroll() computes max = page.scrollHeight - window.innerHeight — the total distance the page can actually scroll — and divides the current window.scrollY by it to get a 0–1 fraction. That fraction becomes strokeDashoffset = CIRC * (1 - pct): at the top of the page the offset equals the full circumference (an empty ring), and at the bottom it's zero (a complete ring). A short .1s linear CSS transition on the property smooths out the otherwise-jittery per-scroll-event updates without introducing any animation loop of its own.

A passive listener, and no per-frame polling

The scroll handler is registered with { passive: true }, telling the browser it will never call preventDefault(), which lets the browser scroll immediately without waiting to see if the handler blocks it — meaningful for scroll smoothness on any page, and free here since the handler only reads scroll position. There's deliberately no requestAnimationFrame loop: the ring only needs to update when a scroll event actually fires, not every frame regardless of whether anything changed.

Show, hide, and jump

The button stays hidden and non-interactive (opacity: 0; pointer-events: none) until window.scrollY passes 240px, at which point a .visible class fades and slides it in — so it never crowds the very top of the page where there's nothing to scroll back to. A click calls window.scrollTo({ top: 0, behavior: 'smooth' }), the native smooth-scroll API, rather than animating scroll position by hand.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly why stroke-dashoffset is set to CIRC * (1 - pct) rather than CIRC * pct — the sign matters, and walking through why makes the stroke-dasharray/stroke-dashoffset mental model click for any future progress-ring work. It is also a good candidate for a performance discussion: ask whether the scroll handler should be throttled or debounced given that scroll events can fire dozens of times per second, and whether the current approach (updating a CSS custom property every event, relying on a short CSS transition to smooth it) is preferable to manually throttling with requestAnimationFrame. For extending it, ask for a version that also displays the scroll percentage as text inside the ring, or one that changes the ring's colour as it approaches 100% to signal "you've reached the end."

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 website footer paired with a fixed "scroll progress" back-to-top button, in plain HTML, CSS, and vanilla JavaScript — no library.

Requirements:
- A standard footer (brand, a couple of link columns, copyright) at the bottom of a scrollable demo page with enough content to actually scroll.
- A circular button fixed to the bottom-right corner of the viewport, built from two concentric SVG circles: a faint static "track" circle and an animated "progress" circle using the stroke-dasharray/stroke-dashoffset technique, with the SVG rotated -90 degrees so the arc fills starting from 12 o'clock.
- On the window's scroll event (registered as passive), compute the scroll fraction as window.scrollY divided by (document scrollHeight minus viewport height), clamp it to 0-1, and set the progress circle's stroke-dashoffset to the circle's circumference times (1 minus that fraction), so the ring visually fills as the user scrolls down the page.
- Apply a short CSS transition (around 0.1s, linear) to stroke-dashoffset so per-scroll-event updates feel smooth rather than jittery, without using any JavaScript animation loop.
- Hide the button (zero opacity, no pointer events) until the user has scrolled past roughly 240px, then fade and slide it into view; clicking it should smooth-scroll the page to the top using the native window.scrollTo API with behavior: 'smooth'.
- Add a small up-arrow icon centered inside the ring, and add a prefers-reduced-motion media query that removes the CSS transition while keeping the ring's fill functional.

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 JSScroll the demo page — the ring in the bottom-right fills as you move down and appears after 240px of scroll.
  2. 2
    Click the ring to jump to topThe page smooth-scrolls to the top using the native scrollTo API, and the ring empties back out as you go.
  3. 3
    Adjust the appearance thresholdChange the 240 value in the JS to show the button sooner or later relative to scroll depth.
  4. 4
    Adjust the ring sizeChange the SVG width/height/radius together, and update CIRC = 2 * Math.PI * r to match the new radius exactly, or the ring will not draw correctly.
  5. 5
    Update the footer contentReplace the brand, link columns, and copyright in the footer with your own — the ring is a separate, independent fixed element.
  6. 6
    Export in your formatClick HTML, JSX, or Tailwind to download the version you need.

Real-world uses

Common Use Cases

Documentation and long-form article pages
Give readers a persistent sense of how much content remains, doubling as a functional back-to-top control once they are done.
Blog posts and tutorials
Pairs naturally with a reading-time estimate at the top of an article — the ring becomes the ongoing, live version of that same information.
Marketing pages with a long scroll
Long single-page sites benefit from a persistent, low-friction way back to the navigation at the top without hunting for a header that has long since scrolled away.
SaaS changelogs and release-note feeds
Useful on pages that are mostly one long list, where "how far down am I" is a real, recurring question for the reader.
Portfolio case studies
A subtle, premium-feeling detail for image- and content-heavy case study pages that runs several screens long.
Studying the SVG stroke-dash technique
A second, real-world application of the same progress-ring math used in the Download Button — useful for seeing the technique applied to a different data source (scroll position instead of a timer).

Got questions?

Frequently Asked Questions

The underlying SVG stroke-dasharray/stroke-dashoffset technique is identical, but the value driving it is different: the Download Button computes progress from an eased timer, while this ring reads real scroll position on every scroll event. There is no animation loop here at all — a CSS transition smooths the value updates instead.

It tells the browser the handler will never call preventDefault(), which lets the browser begin scrolling immediately instead of waiting to see whether the handler would block it. Since this handler only reads scroll position and never prevents default behaviour, passive mode is free correctness and free performance.

You must update the CIRC constant in the JS to 2 * Math.PI * r using the new radius, matching the SVG circle's r attribute. The stroke-dasharray and the dashoffset math both depend on that exact circumference — mismatching them will make the ring appear to fill only partially or overshoot.

No — it stays hidden (opacity: 0, non-interactive) until window.scrollY passes 240px, so it never competes for attention at the very top of the page where scrolling back to the top would be meaningless.

Yes — the scroll event fires for any scroll source, including keyboard (Page Down, spacebar, arrow keys), touch, and JavaScript-driven scrollTo calls elsewhere on the page, since the handler simply reads window.scrollY whenever the browser fires the event.

Click JSX, Vue, or Angular to download the converted component. Attach the scroll listener inside the mount lifecycle (useEffect in React, onMounted in Vue) and return/clean up the listener on unmount to avoid updating state after the component is gone.