Scroll Progress
A thin gradient bar that tracks how far the reader has scrolled.
Scroll inside this card
Designing for momentum
Great interfaces carry momentum. Every scroll, hover and click should feel like it moves the story forward, not like it interrupts it.
A progress indicator quietly answers the question every reader has: how much is left? It reduces anxiety on long pages and rewards people for reading on.
Keep it thin, keep it fast, and tie it directly to scroll position so it never lags behind the reader.
Use it on articles, documentation, changelogs and long-form landing pages — anywhere attention is the scarce resource.
Great interfaces carry momentum. Every scroll, hover and click should feel like it moves the story forward, not like it interrupts it.
A progress indicator quietly answers the question every reader has: how much is left? It reduces anxiety on long pages and rewards people for reading on.
Keep it thin, keep it fast, and tie it directly to scroll position so it never lags behind the reader.
Use it on articles, documentation, changelogs and long-form landing pages — anywhere attention is the scarce resource.
"use client";
import { motion, useScroll, useSpring } from "motion/react";
import { useRef, type RefObject } from "react";
type ScrollProgressProps = {
/** Track a scrollable element instead of the whole page */
container?: RefObject<HTMLElement | null>;
className?: string;
};
export function ScrollProgress({ container, className = "fixed inset-x-0 top-0 z-50" }: ScrollProgressProps) {
const { scrollYProgress } = useScroll(container ? { container } : {});
const scaleX = useSpring(scrollYProgress, { stiffness: 200, damping: 30, restDelta: 0.001 });
return (
<motion.div
aria-hidden
style={{ scaleX }}
className={`h-1 origin-left bg-gradient-to-r from-blue-600 via-violet-500 to-sky-400 ${className}`}
/>
);
}
const paragraphs = [
"Great interfaces carry momentum. Every scroll, hover and click should feel like it moves the story forward, not like it interrupts it.",
"A progress indicator quietly answers the question every reader has: how much is left? It reduces anxiety on long pages and rewards people for reading on.",
"Keep it thin, keep it fast, and tie it directly to scroll position so it never lags behind the reader.",
"Use it on articles, documentation, changelogs and long-form landing pages — anywhere attention is the scarce resource.",
];
export default function Demo() {
const ref = useRef<HTMLDivElement>(null);
return (
<div className="relative w-full max-w-xl overflow-hidden rounded-2xl border border-zinc-200 bg-white shadow-sm">
<ScrollProgress container={ref} className="absolute inset-x-0 top-0 z-10" />
<div ref={ref} className="h-80 overflow-y-auto px-8 py-8">
<p className="text-xs font-medium uppercase tracking-widest text-blue-600">Scroll inside this card</p>
<h3 className="mt-2 text-2xl font-semibold tracking-tight text-zinc-900">Designing for momentum</h3>
{[...paragraphs, ...paragraphs].map((p, i) => (
<p key={i} className="mt-4 leading-relaxed text-zinc-600">
{p}
</p>
))}
</div>
</div>
);
}
Installation
Install the dependencies
$ npm i motion lucide-reactCopy the source into
components/ui/scroll-progress.tsxcomponents/ui/scroll-progress.tsx"use client"; import { motion, useScroll, useSpring } from "motion/react"; import { useRef, type RefObject } from "react"; type ScrollProgressProps = { /** Track a scrollable element instead of the whole page */ container?: RefObject<HTMLElement | null>; className?: string; }; export function ScrollProgress({ container, className = "fixed inset-x-0 top-0 z-50" }: ScrollProgressProps) { const { scrollYProgress } = useScroll(container ? { container } : {}); const scaleX = useSpring(scrollYProgress, { stiffness: 200, damping: 30, restDelta: 0.001 }); return ( <motion.div aria-hidden style={{ scaleX }} className={`h-1 origin-left bg-gradient-to-r from-blue-600 via-violet-500 to-sky-400 ${className}`} /> ); } const paragraphs = [ "Great interfaces carry momentum. Every scroll, hover and click should feel like it moves the story forward, not like it interrupts it.", "A progress indicator quietly answers the question every reader has: how much is left? It reduces anxiety on long pages and rewards people for reading on.", "Keep it thin, keep it fast, and tie it directly to scroll position so it never lags behind the reader.", "Use it on articles, documentation, changelogs and long-form landing pages — anywhere attention is the scarce resource.", ]; export default function Demo() { const ref = useRef<HTMLDivElement>(null); return ( <div className="relative w-full max-w-xl overflow-hidden rounded-2xl border border-zinc-200 bg-white shadow-sm"> <ScrollProgress container={ref} className="absolute inset-x-0 top-0 z-10" /> <div ref={ref} className="h-80 overflow-y-auto px-8 py-8"> <p className="text-xs font-medium uppercase tracking-widest text-blue-600">Scroll inside this card</p> <h3 className="mt-2 text-2xl font-semibold tracking-tight text-zinc-900">Designing for momentum</h3> {[...paragraphs, ...paragraphs].map((p, i) => ( <p key={i} className="mt-4 leading-relaxed text-zinc-600"> {p} </p> ))} </div> </div> ); }Use it
The file exports the component and a
Demoshowing typical usage. Import whichever you need.