ScrollProgress
Show reading progress through a page or scroll container as a bar or ring.
Use it where reading position matters: long articles, docs, changelogs, a long settings page. It answers "how much is left?" at a glance. Skip it on short pages, where there's nothing to track, and don't use it for the progress of a task, like an upload or a build. That's Progress.
Installation
npm i @youj/motionFor an app already set up with npx @youj/cli init, which installs the peers (@youj/core, @youj/motion, motion and Base UI) and wires the provider. New app? Start at Installation.
Usage
With no container, it tracks the page. ScrollProgress doesn't position itself, so place it where it should sit:
import { ScrollProgress } from '@youj/motion'
export function ArticleLayout({ children }: { children: React.ReactNode }) {
return (
<>
<ScrollProgress className="fixed inset-x-0 top-0 z-50" label="Reading progress" />
<article>{children}</article>
</>
)
}The bar fills its width with a 2px accent line, and grows by scaleX from the left.
Examples
Scroll containers
Pass a ref to track an element that scrolls on its own, like the article panel above. Give that element tabIndex={0} and a name, so keyboard users can reach and scroll it.
const article = useRef<HTMLDivElement>(null)
<ScrollProgress variant="ring" container={article} size={28} thickness={2.5} />
<div ref={article} role="region" aria-label="Changelog" tabIndex={0} className="h-64 overflow-y-auto">…</div>When two indicators show the same scroll, as the ring and the bar do here, expose one of them. The bar sits in an aria-hidden wrapper, so a screen reader finds one progress bar, not two.
Ring
variant="ring" draws the progress around a circle, starting at twelve o'clock. size is its diameter and thickness its stroke, both in pixels. Around a back-to-top button, it shows how far down you are and offers the way back up.
The track uses the line token and the arc the accent token, so the ring follows the theme with no props.
Reference
Propsvariant, container, axis… (8)
Props
| Prop | Type | |
|---|---|---|
variant | 'bar' | 'ring'default 'bar' | 'bar' |
container | RefObject<HTMLElement | null>Scroll container to track. Defaults to the page. | – |
axis | 'y' | 'x'default 'y' | 'y' |
spring | SpringToken'snappy''smooth''settle'default 'smooth' | 'smooth' |
size | numberRing diameter in px. default 36 | 36 |
thickness | numberRing stroke width in px. default 3 | 3 |
label | string | – |
className | string | – |
MotionProgress, ring cap · reduced motion
Motion
| Moment | Tokens | ||
|---|---|---|---|
Progressthe bar scales on x; the ring draws its pathLength | spring.smooth0.36s, bounce 0 | 0.36s, bounce 0 | |
| Ring capfades in over the first 0.5%, so an empty ring never shows a dot | no token progress 0 → 0.005 | progress 0 → 0.005 |
Reduced motion
Tracks the scroll position directly, with no spring. The progress still shows.
spring swaps the default for another spring token: snappy follows the scroll more tightly. The spring only smooths the drawing, so a fast fling never makes the indicator lag behind the page for long.
Reduced motion
The indicator tracks the scroll position exactly, with no spring. Nothing else changes: the bar and the ring still show how far you've read, and the back-to-top button in the second preview jumps rather than scrolls smoothly.
AccessibilityKeyboard, ARIA
Accessibility
- It's a
progressbarwith a 0 to 100 range, named bylabel. Name it for what it measures, such as "Reading progress". aria-valuenowfollows the raw scroll position, not the smoothed one, and it's written straight to the attribute, so scrolling never re-renders React.- A scroll container needs
tabIndex={0}and an accessible name to be reachable from the keyboard. - The indicator is information, not decoration. It stays visible under reduced motion.