WebberUI

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.

Loading preview…
npx shadcn@latest add https://webberui.com/r/scroll-progress-set.json

Playground

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.json

Or, 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

PropTypeDefaultDescription
variant"bar" | "ring" | "dots""bar"Progress presentation: a thin top bar, a bottom-right ring, or section dots on the right
containerRefObject<HTMLElement | null>Ref of the nested scroll container; when omitted, the window is the scroll container
sectionsScrollSection[][]Section list for the dots variant; id must match a page element and label is optional
classNamestringAppended to the class of the outermost container

How it works

  • Shared progress: all three variants bind to the same scrollYProgress. bar stretches with scaleX (origin-left), ring closes up via strokeDashoffset and shows the percentage in the center, and dots lights up the current section based on each section's visible ratio.
  • Container mode: when container is passed, a ResizeObserver measures the container's visible height and a sticky anchor 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: dots uses an IntersectionObserver on each section element and picks the one with the highest visible ratio as the current section; the active dot scales up and a layoutId lets 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

  • bar and ring carry role="progressbar" with a live-updating aria-valuenow (0–100).
  • dots are focusable buttons inside a nav, carrying aria-label and aria-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.

On this page