Skip to content
Corsair UI
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-progress

No 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

PropTypeDefault
position

Edge of the window.

"top" | "bottom""top"
timeline

The 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 GitHub
import 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 };