A thin bar showing how far the page, or a scrolling container, has been read.
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-progressCopies 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.jsonWorks, 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.
| Prop | Type | Default |
|---|---|---|
targetA ref to a scrolling container to track. Omit to track the page. | RefObject<HTMLElement | null> | none |
positionWhich edge the bar sits on. | "top" | "bottom" | "top" |
labelAccessible name of the bar. | string | "Reading progress" |
colorColour of the bar: any CSS colour or token. | string | "var(--rd-color-ember-500)" |
thicknessHeight of the bar in pixels. | number | 3 |
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