building a smooth reading progress bar in next.js - By Sourav Mishra (@souravvmishra)
how i built a bouncy reading progress bar with zero extra libraries.
reading progress bars are super popular for long posts. but simple ones look stiff and robotic.
in this post, i'll show you how i built a bouncy, smooth reading progress bar for my blog. the best part? i used zero external libraries, swapping a 30kb package for a tiny 1kb hook.
the goal
we want a thin bar at the top of the screen that tracks how far you scroll.
but we don't want it to jump instantly. we want it to follow your scroll with a little delay and a springy bounce. this makes the ui feel way more alive.
step 1: the basic way
the simple way is just checking window.scrollY against the page height.
a basic react component looks like this:
export function ReadingProgressBar() {
const [progress, setProgress] = useState(0);
useEffect(() => {
const updateProgress = () => {
const currentScroll = window.scrollY;
const scrollHeight = document.documentElement.scrollHeight - window.innerHeight;
if (scrollHeight) {
setProgress((currentScroll / scrollHeight) * 100);
}
};
window.addEventListener("scroll", updateProgress);
return () => window.removeEventListener("scroll", updateProgress);
}, []);
return <div style={{ width: `${progress}%` }} className="h-1 bg-blue-500 fixed top-0" />;
}
it works, but it's super boring. the bar moves instantly and feels stiff.
step 2: adding some bounce
to make it feel better, we add spring physics.
a spring pulls the bar toward your scroll position, but because of "momentum," it takes a split second to get there.
at first, i used framer-motion because it is amazing for animations.
import { motion, useScroll, useSpring } from "framer-motion";
export function ReadingProgressBar() {
const { scrollYProgress } = useScroll();
const scaleX = useSpring(scrollYProgress, {
stiffness: 100,
damping: 30,
restDelta: 0.001
});
return (
<motion.div
className="fixed top-0 inset-x-0 h-1 bg-blue-500 origin-left"
style={{ scaleX }}
/>
);
}
this felt amazing. it bounced perfectly when you scrolled fast and stopped.
but there was one huge problem.
step 3: dropping 30kb of weight
framer-motion is heavy (around 30kb). importing all that just for one tiny bar at the top of the screen felt silly.
why ship 30kb when i can just do the math myself?
i dropped the library and wrote a custom useSpring hook. the physics math is actually super simple:
acceleration = (target - current) * stiffness - velocity * damping
by running this loop on every frame with requestAnimationFrame, i got the exact same bouncy effect with zero dependencies.
the custom hook
here is the tiny hook i wrote:
function useSpring(targetValue: number) {
const [value, setValue] = useState(targetValue);
const state = useRef({ value: targetValue, velocity: 0, target: targetValue });
useEffect(() => {
state.current.target = targetValue;
}, [targetValue]);
useEffect(() => {
// runs the physics formula below...
// acceleration = displacement * stiffness - velocity * damping
// ... using requestAnimationFrame
}, [targetValue]);
return value;
}
you can grab the full code on my github.
why i actually removed it
the code worked perfectly and was super fast. but i ended up taking it off the site.
why? for a minimal blog like mine, a moving bar was just too distracting.
it pulled the eye away from the words. sometimes, adding cool ui stuff just hurts readability. i care more about a clean reading experience than showing off a bouncy animation.
key takeaways
- the basic way: simple math works, but it feels stiff and robotic.
- spring physics: they add a nice bounce and make the ui feel alive.
- custom hooks win: building your own tiny hook saves 30kb over huge libraries.
- know when to stop: sometimes, the best feature is the one you don't add because it distracts the user.
written by sourav mishra, full stack engineer for next.js and ai.
frequently asked questions
q: why not just use framer-motion? it's a great tool, but it's 30kb. a custom hook is only 1kb and saves a lot of bundle size for a tiny feature.
q: does the loop slow down the site? nope! the math runs outside of react's render cycle and stops perfectly when you stop scrolling.
q: what is the difference between a spring and a css transition? css transitions run on a fixed timer. springs use real momentum and math, so they feel much more natural and alive.