Scroll Progress Set
Three scroll indicators in one — a top bar, a bottom-right ring, and section dots on the right, all sharing the same scroll progress.
npx shadcn@latest add https://webberui.com/r/scroll-progress-set.jsonPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<ScrollProgressSet />
Installation
npx shadcn@latest add https://webberui.com/r/scroll-progress-set.jsonOr, once registries are configured in components.json, install it as @webberui/scroll-progress-set.
Usage
ScrollProgress tracks scroll progress with useScroll and can be switched between three presentations. When you pass container, it detects that container's internal scrolling and aligns the indicator with the container's visible area; without it, the whole window is tracked.
import {
ScrollProgress,
type ScrollSection,
} from "@/components/ui/scroll-progress-set";
// A top progress bar tracking the whole page
<ScrollProgress variant="bar" />
// Tracking a specific overflow container
const scrollerRef = React.useRef<HTMLDivElement>(null);
<div ref={scrollerRef} className="h-[400px] overflow-y-auto">
<ScrollProgress variant="ring" container={scrollerRef} />
{/* Content */}
</div>The dots variant requires sections, whose id values must match the id of the corresponding elements on the page:
const sections: ScrollSection[] = [
{ id: "intro", label: "Intro" },
{ id: "usage", label: "Usage" },
];
<ScrollProgress variant="dots" container={scrollerRef} sections={sections} />
<section id="intro">…</section>
<section id="usage">…</section>Props
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "bar" | "ring" | "dots" | "bar" | Progress presentation: a thin top bar, a bottom-right ring, or section dots on the right |
container | RefObject<HTMLElement | null> | — | Ref of the nested scroll container; when omitted, the window is the scroll container |
sections | ScrollSection[] | [] | Section list for the dots variant; id must match a page element and label is optional |
className | string | — | Appended to the class of the outermost container |
How it works
- Shared progress: all three variants bind to the same
scrollYProgress.barstretches withscaleX(origin-left),ringcloses up viastrokeDashoffsetand shows the percentage in the center, anddotslights up the current section based on each section's visible ratio. - Container mode: when
containeris passed, aResizeObservermeasures the container's visible height and astickyanchor carries the overlay, so the indicator stays aligned with the visible area while the container scrolls; nothing is drawn until measurement finishes, which avoids misplacement. - Section detection:
dotsuses anIntersectionObserveron each section element and picks the one with the highest visible ratio as the current section; the active dot scales up and alayoutIdlets the outer ring indicator slide continuously between dots. - Spring smoothing: the progress value passes through
useSpring, so it keeps easing toward the target after scrolling stops. - The component fully cleans up its
IntersectionObserver,ResizeObserver, and Motion event listeners on unmount.
Accessibility
barandringcarryrole="progressbar"with a live-updatingaria-valuenow(0–100).dotsare focusable buttons inside anav, carryingaria-labelandaria-current; clicking one smoothly scrolls to the matching section, and they have a keyboard focus ring.- When the user has "reduce motion" enabled at the system level, progress still updates with the scroll — it simply skips the spring smoothing and the smooth scrolling.
Zoom Parallax
A photo group that zooms through with the scroll — each image scales from the center at its own rate, creating a fly-through depth parallax.
Velocity Skew
Skews and blurs content in real time based on scroll speed, springing back level when the scroll stops, for a reading feel full of kinetic energy.