Dependency-free SVG scroll progress button with smooth scroll-to-top behavior.
It is useful for blogs, landing pages, documentation pages, and long article layouts.
The button shows page scroll progress by drawing an SVG path. When clicked, it scrolls the page back to the top.
Created as part of the development effort for Unboxed.sk.
- Pure JavaScript.
- No runtime dependencies.
- Works with plain CSS, Tailwind CSS, Bootstrap, and React.
- Shared stylesheet with CSS-variable fallbacks.
- Uses SVG path stroke progress.
- Smooth scroll-to-top on click.
- Configurable activation threshold.
- Default activation threshold:
60px. - Supports multiple progress buttons on the same page.
- Safe when no matching element exists.
- SPA-friendly
destroy()method.
<link rel="stylesheet" href="src/scroll-progress.css">
<button class="progress-wrap" aria-label="Scroll back to top">
<svg class="progress-circle" viewBox="-1 -1 102 102" aria-hidden="true">
<path d="M50,1 a49,49 0 0,1 0,98 a49,49 0 0,1 0,-98"></path>
</svg>
<svg class="progress-icon" viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 5l-6 6h4v6h4v-6h4z"></path>
</svg>
</button>
<script src="src/scroll-progress.js"></script>By default, the button becomes active after the page is scrolled 60px.
Override this with data-active-offset:
<button class="progress-wrap" data-active-offset="120" aria-label="Scroll back to top">
...
</button>The active-progress class then appears after 120px of scrolling. Missing, empty, or invalid values fall back to 60.
Use the provided stylesheet:
<link rel="stylesheet" href="src/scroll-progress.css">All values have fallbacks. Override only what a design needs:
<button
class="progress-wrap"
style="
--progress-size: 56px;
--progress-color: #2563eb;
--progress-background: #eff6ff;
--progress-stroke-width: 5;
--progress-icon-size: 26px;
"
>
...
</button>Available variables:
| Variable | Fallback | Controls |
|---|---|---|
--progress-size |
48px |
Button width and height |
--progress-circle-size |
Button size minus 4px |
Progress SVG size |
--progress-icon-size |
24px |
Arrow icon size |
--progress-color |
#0f172a |
Default circle and icon color |
--progress-stroke-color |
currentColor |
Circle color only |
--progress-icon-color |
currentColor |
Icon color only |
--progress-background |
#fff |
Button background |
--progress-stroke-width |
4 |
SVG progress stroke width |
--progress-stroke-linecap |
round |
SVG stroke ends |
--progress-bottom |
24px |
Bottom position |
--progress-right |
24px |
Right position |
--progress-left |
24px |
Left position with .progress-wrap--left |
--progress-z-index |
50 |
Stacking order |
--progress-radius |
999px |
Corner radius |
--progress-border |
0 |
Button border |
--progress-shadow |
Included soft shadow | Box shadow |
--progress-hidden-translate |
12px |
Hidden-state vertical movement |
--progress-transition-duration |
180ms |
Visibility transition |
--progress-focus-color |
#38bdf8 |
Keyboard focus outline |
The stylesheet disables its visibility transition when the user prefers reduced motion.
The optional React component is exported from scroll-progress-button/react.
Import the shared stylesheet once:
import ScrollProgressButton from 'scroll-progress-button/react';
import 'scroll-progress-button/styles.css';
export default function Page() {
return (
<ScrollProgressButton
activeOffset={120}
size={56}
iconSize={26}
strokeWidth={5}
color="#2563eb"
backgroundColor="#eff6ff"
sideOffset={32}
style={{
'--progress-bottom': '32px',
'--progress-shadow': '0 16px 40px rgb(37 99 235 / 20%)'
}}
/>
);
}Common React props:
| Prop | Default | Description |
|---|---|---|
activeOffset |
60 |
Activation distance in pixels |
alwaysVisible |
false |
Keeps the button visible at the page top |
smooth |
true |
Enables smooth scroll unless reduced motion is preferred |
position |
"right" |
Use "left" or "right" |
sideOffset |
CSS fallback | Distance from the selected side |
size |
CSS fallback | Number in pixels or any CSS length |
iconSize |
CSS fallback | Number in pixels or any CSS length |
strokeWidth |
CSS fallback | Progress stroke width |
color |
CSS fallback | Default foreground color |
backgroundColor |
CSS fallback | Button background |
icon |
Included arrow | Any React node used as the icon |
className |
"" |
Additional button classes |
style |
{} |
Inline styles and arbitrary CSS variables |
ariaLabel |
"Scroll back to top" |
Accessible button name |
onClick |
— | Additional click handler; call preventDefault() to cancel scrolling |
Other native button props are passed through to the underlying semantic
<button>.
The script measures the path with:
path.getTotalLength()It then initializes the stroke:
path.style.strokeDasharray = `${length} ${length}`;
path.style.strokeDashoffset = length;At the top of the page, the stroke is fully hidden. At the bottom, strokeDashoffset becomes 0, making the complete stroke visible.
The script automatically initializes every element matching .progress-wrap.
const instances = initScrollProgress('.progress-wrap');Useful in SPA environments:
instances.forEach(instance => instance.destroy());| Attribute | Default | Description |
|---|---|---|
data-active-offset |
60 |
Scroll distance in pixels before .active-progress is added. |
examples/vanilla.htmlexamples/tailwind.html(uses the Tailwind CDN)examples/bootstrap.html(uses the Bootstrap CDN)examples/react.html(uses React from a CDN)
The Vanilla, Tailwind, and Bootstrap examples can be opened directly. Serve the project for the React module example:
npm startModern browsers that support:
querySelectorAllclassListdatasetwindow.scrollTo({ behavior: 'smooth' })- SVG
getTotalLength()
For older browsers, smooth scrolling may degrade to instant scrolling depending on support.
Use a real <button> element where possible:
<button class="progress-wrap" aria-label="Scroll back to top">Avoid using only a decorative <div> unless you also add keyboard behavior and proper ARIA attributes.
MIT