Browse the docs
MotionNo. 83 of 109
Scroll Progress
A reading progress bar that fills as the page scrolls, with CSS scroll-driven animations and no JavaScript.
Installation
pnpm dlx shadcn@latest add @corsair-ui/scroll-progressNo setup needed: the CLI finds @corsair-ui in the shadcn registry directory. The files land in your project as source: edit them like your own code. More in Installation.
- Also adds
- utils
- npm
clsxtailwind-merge
Examples
Inside a scroll box
timeline="nearest", made sticky inside the box.
Day 1. Wind steady from the east, the crew in good spirits, and the bar at the top of this box fills as you scroll the log.
Day 2. Wind steady from the east, the crew in good spirits, and the bar at the top of this box fills as you scroll the log.
Day 3. Wind steady from the east, the crew in good spirits, and the bar at the top of this box fills as you scroll the log.
Day 4. Wind steady from the east, the crew in good spirits, and the bar at the top of this box fills as you scroll the log.
Day 5. Wind steady from the east, the crew in good spirits, and the bar at the top of this box fills as you scroll the log.
Day 6. Wind steady from the east, the crew in good spirits, and the bar at the top of this box fills as you scroll the log.
Day 7. Wind steady from the east, the crew in good spirits, and the bar at the top of this box fills as you scroll the log.
Day 8. Wind steady from the east, the crew in good spirits, and the bar at the top of this box fills as you scroll the log.
Day 9. Wind steady from the east, the crew in good spirits, and the bar at the top of this box fills as you scroll the log.
Day 10. Wind steady from the east, the crew in good spirits, and the bar at the top of this box fills as you scroll the log.
Day 11. Wind steady from the east, the crew in good spirits, and the bar at the top of this box fills as you scroll the log.
Day 12. Wind steady from the east, the crew in good spirits, and the bar at the top of this box fills as you scroll the log.
For the page
Browsers without scroll-driven animations show no bar rather than a wrong one.
API
| Prop | Type | Default |
|---|---|---|
positionEdge of the window. | "top" | "bottom" | "top" |
timelineThe page, or the nearest scroll box (make it sticky there). | "root" | "nearest" | "root" |
Accessibility
- Decorative and hidden from screen readers.
- Pure CSS; browsers without scroll-driven animations show no bar.
Compatibility
- Tailwind CSS
- 3.4 and 4, both checked in CI
- React
- 19
- Rendering
- Works in server components
- Motion
- Respects prefers-reduced-motion
Source
1 file, installed as source you own
scroll-progress.tsxView on GitHubimport type { ComponentProps, CSSProperties } from "react";
import { cn } from "@/registry/default/lib/utils";
interface ScrollProgressProps extends Omit<ComponentProps<"div">, "children"> {
/** Which edge of the viewport the bar sits on. */
position?: "top" | "bottom";
/**
* What it measures: "root" is the page, "nearest" the closest scrolling
* ancestor (give the bar `sticky` or `absolute` positioning inside it).
*/
timeline?: "root" | "nearest";
}
/**
* A reading progress bar that fills as the page scrolls. It is a CSS
* scroll-driven animation of `scaleX`, so there is no JavaScript and no
* scroll listener, and it works in server components. Browsers without
* scroll timelines show nothing rather than a bar stuck at zero. It is
* decorative and hidden from screen readers. It keeps running with
* `prefers-reduced-motion`, since it follows the scroll position rather
* than adding motion of its own.
*
* @example
* <ScrollProgress className="bg-foreground h-1" />
*/
function ScrollProgress({
position = "top",
timeline = "root",
className,
style,
...props
}: ScrollProgressProps) {
return (
<div
aria-hidden="true"
data-slot="scroll-progress"
data-position={position}
className={cn(
"bg-primary pointer-events-none fixed inset-x-0 z-50 h-[3px] origin-left",
position === "bottom" ? "bottom-0" : "top-0",
"supports-[animation-timeline:scroll()]:animate-scroll-progress hidden supports-[animation-timeline:scroll()]:block",
className
)}
style={
{
// After the utility's shorthand, which would reset the timeline.
animationTimeline: timeline === "nearest" ? "scroll(nearest)" : "scroll(root)",
...style,
} as CSSProperties
}
{...props}
/>
);
}
export { ScrollProgress, type ScrollProgressProps };