Scroll Progress

A progress bar that follows the scroll position of the page or of a container.

Scroll to see the progress bar

"use client"

import { cn } from "cn"
import { ArrowDown02Icon } from "@hugeicons/core-free-icons"
import { HugeiconsIcon } from "@hugeicons/react"
import { motion } from "motion/react"
import {
  ScrollProgress,
  ScrollProgressContainer,
  ScrollProgressProvider,
} from "@animate-ui/react/primitives/scroll-progress"

export default function ScrollProgressDemo({
  global = false,
  direction = "vertical",
}: {
  global?: boolean
  direction?: "horizontal" | "vertical"
}) {
  const horizontal = direction === "horizontal"

  return (
    <div className="relative h-72 w-full max-w-md overflow-hidden rounded-xl border">
      <ScrollProgressProvider
        key={`${global}-${direction}`}
        global={global}
        direction={direction}
      >
        <div
          className={cn(
            "z-50",
            global ? "fixed inset-x-0 top-0" : "absolute inset-x-3 bottom-3"
          )}
        >
          <ScrollProgress
            className={cn("h-1.5 bg-foreground", !global && "rounded-full")}
          />
        </div>
        {global ? (
          <div className="flex size-full items-center justify-center">
            <p className="text-sm font-medium">
              Scroll the page to see the progress bar
            </p>
          </div>
        ) : (
          <ScrollProgressContainer
            className={cn(
              "size-full",
              horizontal ? "overflow-x-auto" : "overflow-y-auto"
            )}
          >
            <div className={cn("flex", !horizontal && "flex-col")}>
              <div className="flex h-72 w-full shrink-0 items-center justify-center">
                <p className="flex items-center gap-2 text-sm font-medium">
                  Scroll to see the progress bar
                  <motion.span
                    className={horizontal ? "-rotate-90" : undefined}
                    animate={{ y: [3, -3, 3] }}
                    transition={{
                      duration: 1.25,
                      repeat: Infinity,
                      ease: "easeInOut",
                      type: "keyframes",
                    }}
                  >
                    <HugeiconsIcon
                      icon={ArrowDown02Icon}
                      strokeWidth={2}
                      className="size-5"
                    />
                  </motion.span>
                </p>
              </div>
              <div className="h-72 w-full shrink-0 p-3">
                <div className="size-full rounded-xl bg-accent" />
              </div>
              <div className="h-72 w-full shrink-0" />
              <div className="h-72 w-full shrink-0 p-3">
                <div className="size-full rounded-xl bg-accent" />
              </div>
              <div className="h-72 w-full shrink-0" />
            </div>
          </ScrollProgressContainer>
        )}
      </ScrollProgressProvider>
    </div>
  )
}

Installation

npm install @animate-ui/react motion
pnpm add @animate-ui/react motion
yarn add @animate-ui/react motion
bun add @animate-ui/react motion

Usage

import {
  ScrollProgress,
  ScrollProgressContainer,
  ScrollProgressProvider,
} from '@animate-ui/react/primitives/scroll-progress';

// A scrollable container.
export function Article() {
  return (
    <ScrollProgressProvider>
      <ScrollProgress className="h-1 bg-foreground" />
      <ScrollProgressContainer className="h-96 overflow-y-auto">
        …
      </ScrollProgressContainer>
    </ScrollProgressProvider>
  );
}

// The whole page.
export function PageProgress() {
  return (
    <ScrollProgressProvider global>
      <ScrollProgress className="fixed inset-x-0 top-0 h-1 bg-foreground" />
    </ScrollProgressProvider>
  );
}

Animation

  • The bar follows the scroll position on a spring without overshoot (stiffness 250, damping 40), so it glides instead of jumping on wheel steps.
  • By default it grows in width (mode="width", from 0 to 100%), so rounded ends keep their shape; height does the same vertically. scaleX and scaleY scale it with a transform instead, from its transform-origin (the center unless you set one, e.g. origin-left).
  • Scrolling never re-renders React: the bar is driven by motion values.
  • Under reduced motion, the bar tracks the scroll position without easing.

API Reference

ScrollProgressProvider

PropTypeDefault
global?
booleanfalse
direction?
'vertical' | 'horizontal''vertical'
transition?
SpringOptions{ stiffness: 250, damping: 40, bounce: 0 }
children
React.ReactNode-

ScrollProgress

PropTypeDefault
asChild?
booleanfalse
mode?
'width' | 'height' | 'scaleX' | 'scaleY''width'
...props?
HTMLMotionProps<'div'>-

ScrollProgressContainer

The scrollable element (without global). Make it scroll with overflow-y-auto.

PropTypeDefault
asChild?
booleanfalse
...props?
HTMLMotionProps<'div'>-

useScrollProgress

Returns progress (raw, 0 to 1) and scale (smoothed) motion values, with direction, global and the containerRef.

Built by Skyleen. The source code is available on GitHub.

Last updated: 10/18/2026

On this page