---
title: Progress
description: Thin scroll line under the nav, empty left to full right.
group: Scroll
order: 24
new: true
---

A quiet bar under the nav that fills with scroll. Left is the top of the page. Right is the end.

```tsx
"use client"

import { useRef } from "react"
import { Navbar01 } from "@/components/blocks/navbar/navbar-01"
import { Progress } from "@/components/motion/progress"

export function Example() {
  const scrollRef = useRef<HTMLDivElement>(null)

  return (
    <div className="flex h-[38rem] flex-col overflow-hidden rounded-xl border">
      <Navbar01 />
      <Progress target={scrollRef} color="#3b82f6" />
      <div ref={scrollRef} className="min-h-0 flex-1 overflow-y-auto px-5 py-8">
        {/* long page content */}
      </div>
    </div>
  )
}
```

## Installation

```bash
npx @arctis-sh/@arctis-sh/ui@latest add scroll-progress
```

## Usage

```tsx
import { useRef } from "react"
import { Navbar01 } from "@/components/blocks/navbar/navbar-01"
import { Progress } from "@/components/motion/progress"
```

```tsx
const scrollRef = useRef<HTMLDivElement>(null)

<>
  <Navbar01 />
  <Progress target={scrollRef} color="#3b82f6" />
  <div ref={scrollRef} className="overflow-y-auto">
    {/* page */}
  </div>
</>
```

Omit `target` to track the window instead.

## API Reference

### Progress

| Prop | Type | Default |
| --- | --- | --- |
| target | RefObject<HTMLElement | null> |
| color | string | currentColor |
| height | number | 1 |
| className | string | — |

Also accepts the other native `div` attributes. Height is in pixels.

## Source

```tsx
"use client";

import "@/lib/motion-styles";

import {
  useEffect,
  useState,
  type ComponentProps,
  type CSSProperties,
  type RefObject,
} from "react";
import { cn } from "@/lib/utils";

export type ProgressProps = Omit<ComponentProps<"div">, "children" | "color"> & {
  /**
   * Scroll container to track.
   * Omit to track the window.
   */
  target?: RefObject<HTMLElement | null>;
  /** Fill color. */
  color?: string;
  /** Bar thickness in px. */
  height?: number;
};

function scrollMetrics(target?: HTMLElement | null) {
  if (target) {
    const max = target.scrollHeight - target.clientHeight;
    if (max <= 0) return 0;
    return Math.min(Math.max(target.scrollTop / max, 0), 1);
  }
  const doc = document.documentElement;
  const max = doc.scrollHeight - window.innerHeight;
  if (max <= 0) return 0;
  return Math.min(Math.max(window.scrollY / max, 0), 1);
}

function Progress({
  className,
  target,
  color = "currentColor",
  height = 1,
  style,
  ...props
}: ProgressProps) {
  const [value, setValue] = useState(0);

  useEffect(() => {
    let disposed = false;
    let detach: (() => void) | undefined;
    let raf = 0;

    function measure() {
      setValue(scrollMetrics(target ? target.current : null));
    }

    function attach(node: HTMLElement | null) {
      measure();

      if (node) {
        node.addEventListener("scroll", measure, { passive: true });
        const observer = new ResizeObserver(measure);
        observer.observe(node);
        return () => {
          node.removeEventListener("scroll", measure);
          observer.disconnect();
        };
      }

      if (target) return;

      window.addEventListener("scroll", measure, { passive: true });
      window.addEventListener("resize", measure);
      return () => {
        window.removeEventListener("scroll", measure);
        window.removeEventListener("resize", measure);
      };
    }

    function bind() {
      const node = target?.current ?? null;
      if (target && !node) {
        raf = window.requestAnimationFrame(bind);
        return;
      }
      if (disposed) return;
      detach = attach(node);
    }

    bind();

    return () => {
      disposed = true;
      window.cancelAnimationFrame(raf);
      detach?.();
    };
  }, [target]);

  return (
    <div
      data-slot="progress"
      role="progressbar"
      aria-valuemin={0}
      aria-valuemax={100}
      aria-valuenow={Math.round(value * 100)}
      aria-label="Scroll progress"
      className={cn("arctis-progress relative w-full overflow-hidden", className)}
      style={
        {
          height,
          ...style,
        } as CSSProperties
      }
      {...props}
    >
      <span
        className="arctis-progress-bar absolute inset-y-0 left-0 w-full origin-left"
        style={
          {
            background: color,
            transform: `scaleX(${value})`,
          } as CSSProperties
        }
      />
    </div>
  );
}

export { Progress };
```
