# Scroll Progress

> Top gradient bar that fills with scroll progress; bind it to any scroll container.

- Author: Atelier UI
- License: MIT
- Page: https://chromotion.dev/en/components/scroll-progress
- Site: Atelier UI (https://chromotion.dev)

## Install

```bash
npx shadcn@latest add https://chromotion.dev/r/scroll-progress.json
```

Stack: React + TypeScript + Tailwind CSS v4, shadcn/ui conventions. `cn` comes from `@/lib/utils`.

## Dependencies

No extra dependencies.

## Usage

```tsx title="components/scroll-progress-demo.tsx"
"use client"

import * as React from "react"

import { ScrollProgress } from "@/components/scroll-progress"

export default function ScrollProgressDemo() {
  const scroller = React.useRef<HTMLDivElement>(null)

  return (
    <div className="relative w-full max-w-xl overflow-hidden rounded-2xl border bg-card">
      <ScrollProgress scrollTarget={scroller} className="absolute" />
      <div ref={scroller} className="h-80 overflow-y-auto p-6">
        <h3 className="text-lg font-semibold">Designing calm interfaces</h3>
        <p className="mt-1 text-xs text-muted-foreground">8 min read — scroll to fill the bar</p>
        {Array.from({ length: 7 }, (_, index) => (
          <div key={index} className="mt-5 grid gap-2">
            <div className="h-3 w-full rounded bg-muted/70" />
            <div className="h-3 w-11/12 rounded bg-muted/70" />
            <div className="h-3 w-4/5 rounded bg-muted/70" />
            <div className="h-20 rounded-lg bg-muted/50" />
          </div>
        ))}
      </div>
    </div>
  )
}
```

## Source

```tsx title="components/scroll-progress.tsx"
"use client"

import * as React from "react"

import { cn } from "@/lib/utils"

export interface ScrollProgressProps extends Omit<React.ComponentProps<"div">, "children"> {
  /** Scroll container to track; the window when omitted */
  scrollTarget?: React.RefObject<HTMLElement | null>
}

export function ScrollProgress({ scrollTarget, className, ...props }: ScrollProgressProps) {
  const [progress, setProgress] = React.useState(0)

  React.useEffect(() => {
    const element = scrollTarget?.current
    const source: HTMLElement | Window = element ?? window
    const update = () => {
      const scrolled = element ? element.scrollTop : window.scrollY
      const total = element ? element.scrollHeight - element.clientHeight : document.documentElement.scrollHeight - window.innerHeight
      setProgress(total > 0 ? Math.min(1, Math.max(0, scrolled / total)) : 0)
    }
    const frame = requestAnimationFrame(update)
    source.addEventListener("scroll", update, { passive: true })
    window.addEventListener("resize", update)
    return () => {
      cancelAnimationFrame(frame)
      source.removeEventListener("scroll", update)
      window.removeEventListener("resize", update)
    }
  }, [scrollTarget])

  return (
    <div
      role="progressbar"
      aria-label="Reading progress"
      aria-valuemin={0}
      aria-valuemax={100}
      aria-valuenow={Math.round(progress * 100)}
      className={cn("pointer-events-none fixed inset-x-0 top-0 z-50 h-1 bg-transparent", className)}
      {...props}
    >
      <div className="h-full origin-left bg-gradient-to-r from-sky-500 via-indigo-500 to-fuchsia-500" style={{ transform: `scaleX(${progress})` }} />
    </div>
  )
}
```
