useScrollProgress

@sushindustries/ui

How far one element has travelled up the viewport, 0 to 1, once per frame. Gated by an observer so an off-screen element costs nothing.

6 min read
View as MarkdownEdit on GitHub

Install commands are on Home, attached from the registry - they are not written here, because a second copy is a copy that goes stale. This tab starts after the install worked.

Use it

import { useRef, useCallback } from "react";
import { useScrollProgress } from "@sushindustries/ui";

export function ProgressBar() {
	const stageRef = useRef<HTMLDivElement>(null);
	const barRef = useRef<HTMLDivElement>(null);

	const show = useCallback((progress: number) => {
		if (barRef.current) barRef.current.style.transform = `scaleX(${progress})`;
	}, []);

	useScrollProgress(stageRef, show);

	return (
		<div ref={stageRef} style={{ minHeight: "200vh" }}>
			<div ref={barRef} style={{ height: 6, background: "var(--fg)" }} />
		</div>
	);
}
tsx

What you should see

Nothing until you scroll - the hook itself renders no markup. As the tall stageRef element rises up the viewport, show fires roughly once per frame, and the bar's scaleX climbs from 0 to 1 by the time the element's top reaches 55% up the screen (the default finishAt). Scroll back down and it reverses smoothly, because progressProgressThe native progress element, restyled - omit value and the indeterminate state is real.@sushindustries/ui · content is a direct read of position, not a one-shot trigger.

If nothing happens

The most common cause is stageRef never attaching to anything tall enough to scroll - a zero-height element never crosses the viewport, so progressProgressThe native progress element, restyled - omit value and the indeterminate state is real.@sushindustries/ui · content never leaves 0. The other is whenVisible doing exactly what it is meant to: while the element is off screen the IntersectionObserver is not gating true, so the scroll listener never runs and onProgress is never called until it comes into view.