Scroll Progress
A free reading progress bar that fills a scroll section indicator as you read.
- Automated axe check
- Reduced-motion tested
- SSR build verified
- GSAP
- 2 runtime packages
Scroll Progress is a free reading progress bar for Next.js and React: a fixed bar across the top of the viewport whose fill scales from empty to full as the page scrolls, driven by a single GSAP ScrollTrigger scrub and styled with Tailwind. By default it tracks the whole document; pass a target selector and it maps its range to one article instead.
The fill is one scaleX transform scrubbed directly from native scroll, so it stays exactly in step with the scrollbar at any speed and never runs its own animation loop. The bar ignores the pointer, is hidden from assistive technology as pure decoration, and with prefers-reduced-motion enabled no scroll-linked animation is created at all: the fill is hidden and the track renders static.
Reading progress
Look at the top edge of the viewport
The bar you can see fixed across the very top of this page is the demo. As you scroll down, its fill scales from empty to full, mapped exactly to how far through the document you are: empty at the top, full when you reach the bottom.
There is nothing to configure to get this behaviour. Drop the component anywhere on a page and it tracks the document scroll by default, driven by a single GSAP ScrollTrigger scrub on one scaleX transform, so it stays perfectly in step with the scrollbar at any speed.
Long reads are where a progress bar earns its place. On an article or changelog it answers the one question every reader silently asks: how much is left? Pass a target selector and the bar maps its range to that single article instead of the whole page.
The bar itself stays out of the way. It is a few pixels tall, ignores the pointer entirely and is hidden from assistive technology, because it repeats information the scrollbar already conveys rather than adding anything new.
With reduced motion enabled the fill is hidden and no scroll-linked animation is created at all; the track renders static and empty. Keep scrolling to the end of this page and watch the fill arrive at exactly one hundred percent as you do.
Install
Add the @te registry to your components.json once:
{
"registries": {
"@te": "https://templateempire.io/r/{name}.json"
}
}Then install the component:
npx shadcn@latest add @te/scroll-progressUsage
import { ScrollProgress } from "@/components/scroll-progress";
export default function Page() {
return (
<>
<ScrollProgress target="#article" />
<article id="article">
<h1>The long read</h1>
<p>The bar fills as this article scrolls through the viewport.</p>
</article>
</>
);
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
| target | string | undefined | CSS selector of the article to track. The fill maps that element's scroll range to 0 to 100%. Tracks the whole document when unset. |
| className | string | undefined | Additional classes merged onto the fixed bar container. |
Customisation
Scroll Progress declares two component tokens: --progress-color sets the fill colour and defaults to the brand token, and --progress-height sets the bar height and defaults to 3px. Override either from :root, or from any ancestor of the bar, without touching the component:
:root {
--progress-color: var(--color-accent-400);
--progress-height: 5px;
}Frequently asked questions
- Is Scroll Progress free for commercial use?
- Yes. Scroll Progress is MIT licensed, so you can use it in personal and commercial projects without attribution.
- Does Scroll Progress hurt performance or accessibility?
- No. The fill is a single scaleX transform scrubbed by one GSAP ScrollTrigger tween, so there is no per-frame JavaScript loop of its own and no layout or paint beyond the compositor. The bar is a few pixels tall, sits above content with pointer events disabled so it never blocks reading or interaction, and is marked aria-hidden because it repeats what the scrollbar already conveys; a progressbar role is deliberately not used since per-frame value updates would spam screen readers. With reduced motion enabled the fill is hidden and no scroll-linked animation is created.
- How do I track a single article instead of the whole page?
- Pass a CSS selector via the target prop, for example target set to #article. The fill then maps its 0 to 100% range to that element, starting when its top reaches the top of the viewport and finishing when its bottom reaches the bottom. Leave target unset to track the whole document.