Skip to content

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/motion

import { ScrollProgress } from '@youj/motion'

For 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.

Reference

Propsvariant, container, axis… (8)

Props

ScrollProgress props
PropType
variant'bar' | 'ring'

default 'bar'

'bar'
containerRefObject<HTMLElement | null>

Scroll container to track. Defaults to the page.

–
axis'y' | 'x'

default 'y'

'y'
springSpringToken
'snappy''smooth''settle'

default 'smooth'

'smooth'
sizenumber

Ring diameter in px.

default 36

36
thicknessnumber

Ring stroke width in px.

default 3

3
labelstring–
classNamestring–
MotionProgress, ring cap · reduced motion

Motion

Motion tokens
MomentTokens
Progressthe bar scales on x; the ring draws its pathLength
spring.smooth

0.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 progressbar with a 0 to 100 range, named by label. Name it for what it measures, such as "Reading progress".
  • aria-valuenow follows 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.