Circular progress

A ring in the corner fills with your scroll position and doubles as a back-to-top button.

How it works

An SVG circle uses stroke-dasharray equal to its circumference. Scroll progress sets stroke-dashoffset from the full length to zero, and a number inside shows the percentage. Clicking scrolls smoothly to the top.

Full file
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Circular progress</title>
<style>
*, *::before, *::after { box-sizing: border-box; }
html, body { margin: 0; }
body { font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; -webkit-font-smoothing: antialiased; }

body { background: #101826; color: #dbe7ff; }
.long { max-width: 680px; margin: 0 auto; padding: 10vh 24px 20vh; }
.long section { height: 50vh; border-bottom: 1px solid #23324d; display: flex; align-items: center; font-size: 2rem; font-weight: 700; }
.ring { position: fixed; right: 24px; bottom: 24px; width: 84px; height: 84px; border: 0; padding: 0; background: #182338; border-radius: 50%; color: #fff; cursor: pointer; }
.ring svg { position: absolute; inset: 0; transform: rotate(-90deg); }
.ring circle { fill: none; stroke-width: 7; }
.track { stroke: #2a3a5a; }
.fill { stroke: #5ef2c2; stroke-linecap: round; stroke-dasharray: 276.46; stroke-dashoffset: 276.46; }
.ring span { position: relative; font: 700 1.2rem system-ui, sans-serif; }
.ring span::after { content: '%'; font-size: .7em; opacity: .7; }
</style>
</head>
<body>
<button class="ring" id="ring" aria-label="Back to top">
  <svg viewBox="0 0 100 100"><circle class="track" cx="50" cy="50" r="44"/><circle class="fill" id="fill" cx="50" cy="50" r="44"/></svg>
  <span id="pct">0</span>
</button>
<main class="long" id="long"></main>
<script>
const long = document.getElementById('long');
for (let i = 1; i <= 12; i++) {
  const s = document.createElement('section');
  s.textContent = 'Section ' + i;
  long.appendChild(s);
}
const fill = document.getElementById('fill');
const pct = document.getElementById('pct');
const C = 2 * Math.PI * 44;
function update() {
  const max = document.documentElement.scrollHeight - innerHeight;
  const p = max > 0 ? scrollY / max : 0;
  fill.style.strokeDashoffset = (C * (1 - p)).toFixed(2);
  pct.textContent = Math.round(p * 100);
}
addEventListener('scroll', update, { passive: true });
document.getElementById('ring').onclick = () => scrollTo({ top: 0, behavior: 'smooth' });
update();
</script>
</body>
</html>

More progress effects