Skip to content

Scroll Progress

A thin bar showing how far the page, or a scrolling container, has been read.

Motionv0.1.0experimentalWCAG 2.2 AAView spec

Optional, and kept apart.

Nothing here is included in a project unless you add it, and the plain component it builds on is unchanged. This effect is decoration: it is hidden from screen readers, stands still for visitors who prefer reduced motion, and can be paused. Use Still on a preview to see that version.

Paragraph 1. Scroll this panel and the bar tracks how much of it you have read. The bar is a progress bar for assistive technology, and it updates a few times a second at most.

Paragraph 2. Scroll this panel and the bar tracks how much of it you have read. The bar is a progress bar for assistive technology, and it updates a few times a second at most.

Paragraph 3. Scroll this panel and the bar tracks how much of it you have read. The bar is a progress bar for assistive technology, and it updates a few times a second at most.

Paragraph 4. Scroll this panel and the bar tracks how much of it you have read. The bar is a progress bar for assistive technology, and it updates a few times a second at most.

Paragraph 5. Scroll this panel and the bar tracks how much of it you have read. The bar is a progress bar for assistive technology, and it updates a few times a second at most.

Paragraph 6. Scroll this panel and the bar tracks how much of it you have read. The bar is a progress bar for assistive technology, and it updates a few times a second at most.

Paragraph 7. Scroll this panel and the bar tracks how much of it you have read. The bar is a progress bar for assistive technology, and it updates a few times a second at most.

Paragraph 8. Scroll this panel and the bar tracks how much of it you have read. The bar is a progress bar for assistive technology, and it updates a few times a second at most.

import { useRef } from "react";
import { ScrollProgress } from "@rdloom/react";

export default function ScrollProgressContainer() {
  const scroller = useRef<HTMLDivElement>(null);
  return (
    <div className="flex w-full justify-center">
      <div className="relative w-full max-w-md overflow-hidden rounded-xl border border-[var(--rd-color-border-default)] bg-[var(--rd-color-surface-default)]">
        <ScrollProgress target={scroller} label="Panel progress" />
        <div ref={scroller} tabIndex={0} aria-label="Scrollable panel" className="h-48 overflow-y-auto p-4 text-sm text-[var(--rd-color-text-default)]">
          {Array.from({ length: 8 }, (_, i) => (
            <p key={i} className="mb-3 text-[var(--rd-color-text-muted)]">
              Paragraph {i + 1}. Scroll this panel and the bar tracks how much of it you have read. The bar is a progress bar for
              assistive technology, and it updates a few times a second at most.
            </p>
          ))}
        </div>
      </div>
    </div>
  );
}

Installation

npx rdloom add scroll-progress

Copies the source into src/components/rdloom. Edit it freely: rdloom upgrade merges later versions into your changes.

It also adds rdloom-motion.css next to your tokens and imports it from the tokens file, once: no setup, and no inline styles. The plain components you already use are untouched.

Use another registry client
npx shadcn@latest add https://rdloom.vimalbhatt.com/r/scroll-progress.json

Works, but without upgrade tracking, and you import the motion CSS yourself.

Usage

import { ScrollProgress } from "@rdloom/react";

<div className="flex w-full justify-center">
  <div className="relative w-full max-w-md overflow-hidden rounded-xl border border-[var(--rd-color-border-default)] bg-[var(--rd-color-surface-default)]">
    <ScrollProgress target={scroller} label="Panel progress" />
    <div ref={scroller} tabIndex={0} aria-label="Scrollable panel" className="h-48 overflow-y-auto p-4 text-sm text-[var(--rd-color-text-default)]">
      {Array.from({ length: 8 }, (_, i) => (
        <p key={i} className="mb-3 text-[var(--rd-color-text-muted)]">
          Paragraph {i + 1}. Scroll this panel and the bar tracks how much of it you have read. The bar is a progress bar for
          assistive technology, and it updates a few times a second at most.
        </p>
      ))}
    </div>
  </div>
</div>

Bottom

Paragraph 1. Scroll this panel and the bar tracks how much of it you have read. The bar is a progress bar for assistive technology, and it updates a few times a second at most.

Paragraph 2. Scroll this panel and the bar tracks how much of it you have read. The bar is a progress bar for assistive technology, and it updates a few times a second at most.

Paragraph 3. Scroll this panel and the bar tracks how much of it you have read. The bar is a progress bar for assistive technology, and it updates a few times a second at most.

Paragraph 4. Scroll this panel and the bar tracks how much of it you have read. The bar is a progress bar for assistive technology, and it updates a few times a second at most.

Paragraph 5. Scroll this panel and the bar tracks how much of it you have read. The bar is a progress bar for assistive technology, and it updates a few times a second at most.

Paragraph 6. Scroll this panel and the bar tracks how much of it you have read. The bar is a progress bar for assistive technology, and it updates a few times a second at most.

Paragraph 7. Scroll this panel and the bar tracks how much of it you have read. The bar is a progress bar for assistive technology, and it updates a few times a second at most.

Paragraph 8. Scroll this panel and the bar tracks how much of it you have read. The bar is a progress bar for assistive technology, and it updates a few times a second at most.

import { useRef } from "react";
import { ScrollProgress } from "@rdloom/react";

export default function ScrollProgressBottom() {
  const scroller = useRef<HTMLDivElement>(null);
  return (
    <div className="flex w-full justify-center">
      <div className="relative w-full max-w-md overflow-hidden rounded-xl border border-[var(--rd-color-border-default)] bg-[var(--rd-color-surface-default)]">
        <ScrollProgress target={scroller} position="bottom" label="Panel progress" />
        <div ref={scroller} tabIndex={0} aria-label="Scrollable panel with a bottom bar" className="h-48 overflow-y-auto p-4 text-sm text-[var(--rd-color-text-default)]">
          {Array.from({ length: 8 }, (_, i) => (
            <p key={i} className="mb-3 text-[var(--rd-color-text-muted)]">
              Paragraph {i + 1}. Scroll this panel and the bar tracks how much of it you have read. The bar is a progress bar for
              assistive technology, and it updates a few times a second at most.
            </p>
          ))}
        </div>
      </div>
    </div>
  );
}

Colored

Paragraph 1. Scroll this panel and the bar tracks how much of it you have read. The bar is a progress bar for assistive technology, and it updates a few times a second at most.

Paragraph 2. Scroll this panel and the bar tracks how much of it you have read. The bar is a progress bar for assistive technology, and it updates a few times a second at most.

Paragraph 3. Scroll this panel and the bar tracks how much of it you have read. The bar is a progress bar for assistive technology, and it updates a few times a second at most.

Paragraph 4. Scroll this panel and the bar tracks how much of it you have read. The bar is a progress bar for assistive technology, and it updates a few times a second at most.

Paragraph 5. Scroll this panel and the bar tracks how much of it you have read. The bar is a progress bar for assistive technology, and it updates a few times a second at most.

Paragraph 6. Scroll this panel and the bar tracks how much of it you have read. The bar is a progress bar for assistive technology, and it updates a few times a second at most.

Paragraph 7. Scroll this panel and the bar tracks how much of it you have read. The bar is a progress bar for assistive technology, and it updates a few times a second at most.

Paragraph 8. Scroll this panel and the bar tracks how much of it you have read. The bar is a progress bar for assistive technology, and it updates a few times a second at most.

import { useRef } from "react";
import { ScrollProgress } from "@rdloom/react";

export default function ScrollProgressColored() {
  const scroller = useRef<HTMLDivElement>(null);
  return (
    <div className="flex w-full justify-center">
      <div className="relative w-full max-w-md overflow-hidden rounded-xl border border-[var(--rd-color-border-default)] bg-[var(--rd-color-surface-default)]">
        <ScrollProgress target={scroller} color="var(--rd-color-ember-600)" thickness={5} label="Panel progress" />
        <div ref={scroller} tabIndex={0} aria-label="Scrollable panel with a thick bar" className="h-48 overflow-y-auto p-4 text-sm text-[var(--rd-color-text-default)]">
          {Array.from({ length: 8 }, (_, i) => (
            <p key={i} className="mb-3 text-[var(--rd-color-text-muted)]">
              Paragraph {i + 1}. Scroll this panel and the bar tracks how much of it you have read. The bar is a progress bar for
              assistive technology, and it updates a few times a second at most.
            </p>
          ))}
        </div>
      </div>
    </div>
  );
}

API Reference

Defined by the spec. Components also accept the props of the React Aria component they wrap.

PropTypeDefault
target

A ref to a scrolling container to track. Omit to track the page.

RefObject<HTMLElement | null>none
position

Which edge the bar sits on.

"top" | "bottom""top"
label

Accessible name of the bar.

string"Reading progress"
color

Colour of the bar: any CSS colour or token.

string"var(--rd-color-ember-500)"
thickness

Height of the bar in pixels.

number3

Accessibility

Role progressbar, WCAG 2.2 AA. Tested with axe and keyboard tests; screen reader checks are in the audit checklist.

Keyboard

Not interactive.

Screen readers announce

  • Announced as a progress bar with its percentage when the reader queries it
  • Not a live region: it does not interrupt reading

What your code must do

  • role=progressbar with an accessible name and aria-valuemin 0, aria-valuemax 100 and aria-valuenow updated as the reader scrolls
  • Updates to aria-valuenow are throttled so assistive technology is not flooded
  • Never focusable and pointer-events none, so content under it stays usable
  • For reduced motion the bar still shows progress but moves instantly, with no easing
  • Forced-colors mode draws the bar in the system Highlight colour
  • The bar is supplementary: it must not be the only way to know where you are

Guidelines

Use it when

  • Long articles and documentation pages
  • A scrolling panel where position matters

Avoid it when

  • Short pages that barely scroll
  • As a replacement for real navigation

Don't

  • Using it as the only position cue
  • A thickness so large it covers a fixed header

Design tokens

The semantic tokens this component uses. Change them once and every component follows; see Design tokens.

  • --rd-color-border-default