Scroll Progress
Reading progress as a lit tube on the window edge, or a ring for back to top. Follows the page or any container.
New in 1.7.
RFC 0042: At-least-once webhooks
Mira Okafor · 6 min read
Installation
npx shadcn@latest add https://neonkit.heyaadi.com/r/scroll-progress.jsonUsage
import { ScrollProgress } from "@/components/ui/scroll-progress"
// Follows the page, pinned to the top of the window
<ScrollProgress />
// Follows a scroll container
<ScrollProgress target={articleRef} />Examples
Ring with a reading
onProgressChange reports whole percentages, so the label renders only when the number changes.
Thin, on a log
This page
Without a target it follows the window. This one is pinned to the bottom edge.
API reference
A progressbar. Also accepts div attributes; children render in the middle of the ring.
| Prop | Type | Default |
|---|---|---|
target | RefObject<HTMLElement>A scroll container to follow. | the page |
variant | "bar"A tube along an edge, or a ring. | "bar" |
fixed | booleanPin the bar to the viewport edge. | !target |
position | "top"Edge a fixed bar sits on. | "top" |
thickness | numberTube thickness in pixels. | 3 (bar), 2 (ring) |
size | numberRing diameter in pixels. | 44 |
onProgressChange | (percent: number) => voidWhole-number percentage, 0 to 100, when it changes. | None |
aria-label | stringName of the progressbar. | "Reading progress" |
color | NeonColorColor of the tube. | inherited |
Accessibility
- It's a progressbar with aria-valuenow in whole percent, updated only when the number changes.
- Scroll writes go straight to the DOM once per frame: a translate on the lit tube for the bar, a dash and a rotation for the ring. Nothing lays out per frame and React doesn't re-render while you scroll.
- The lit tube is translated rather than scaled, so its white-hot tip and halo never stretch. At 100% the whole tube burns a little brighter.
- A ResizeObserver catches content that grows or shrinks without a scroll, such as images loading.
- Inside a button the ring's children are the button's content; give the button its own aria-label.