Progress

Progress bars show either determinate or indeterminate progress of an operation over time.

"use client"

import * as React from "react"
import { cn } from "cn"
import {
  Label as LabelPrimitive,
  ProgressBar as ProgressPrimitive,
  type LabelProps,
  type ProgressBarProps as ProgressPrimitiveProps,
} from "@animate-ui/react/primitives/react-aria"

type ProgressContextValue = {
  percentage?: number
  isIndeterminate: boolean
  valueText?: string
}

const ProgressContext = React.createContext<ProgressContextValue | null>(null)

function useProgress() {
  const context = React.useContext(ProgressContext)
  if (!context) {
    throw new Error("useProgress must be used within a Progress.")
  }

  return context
}

function ProgressContent({
  children,
  percentage,
  isIndeterminate,
  valueText,
}: ProgressContextValue & {
  children?: React.ReactNode
}) {
  const context = React.useMemo(
    () => ({ percentage, isIndeterminate, valueText }),
    [percentage, isIndeterminate, valueText]
  )

  return (
    <ProgressContext value={context}>
      {children}
      <ProgressTrack>
        <ProgressIndicator />
      </ProgressTrack>
    </ProgressContext>
  )
}

function Progress({
  className,
  children,
  ...props
}: Omit<ProgressPrimitiveProps, "children" | "className"> & {
  children?: React.ReactNode
  className?: string
}) {
  return (
    <ProgressPrimitive
      data-slot="progress"
      className={cn("flex flex-wrap gap-3", className)}
      {...props}
    >
      {({ percentage, valueText, isIndeterminate }) => (
        <ProgressContent
          percentage={percentage}
          valueText={valueText}
          isIndeterminate={isIndeterminate}
        >
          {children}
        </ProgressContent>
      )}
    </ProgressPrimitive>
  )
}

function ProgressTrack({ className, ...props }: React.ComponentProps<"span">) {
  return (
    <span
      className={cn(
        "bg-muted h-1 rounded-full relative flex w-full items-center overflow-x-hidden",
        className
      )}
      data-slot="progress-track"
      {...props}
    />
  )
}

function ProgressIndicator({
  className,
  style,
  ...props
}: React.ComponentProps<"span">) {
  const { percentage, isIndeterminate } = useProgress()

  return (
    <span
      data-slot="progress-indicator"
      className={cn("bg-primary h-full", className)}
      style={{
        ...style,
        width: `${isIndeterminate ? 100 : (percentage ?? 0)}%`,
      }}
      {...props}
    />
  )
}

function ProgressLabel({ className, ...props }: LabelProps) {
  return (
    <LabelPrimitive
      className={cn("text-sm font-medium", className)}
      data-slot="progress-label"
      {...props}
    />
  )
}

function ProgressValue({
  className,
  children,
  ...props
}: Omit<React.ComponentProps<"span">, "children"> & {
  children?: (value: string) => React.ReactNode
}) {
  const { valueText } = useProgress()
  return (
    <span
      className={cn("text-muted-foreground ml-auto text-sm tabular-nums", className)}
      data-slot="progress-value"
      {...props}
    >
      {children && valueText != null ? children(valueText) : valueText}
    </span>
  )
}

export {
  Progress,
  ProgressTrack,
  ProgressIndicator,
  ProgressLabel,
  ProgressValue,
}

export default function ProgressDemo() {
  const [progress, setProgress] = React.useState(13)

  React.useEffect(() => {
    const timer = setTimeout(() => setProgress(66), 500)
    return () => clearTimeout(timer)
  }, [])

  return <Progress aria-label="Loading" value={progress} className="w-[60%]" />
}

Installation

Install the packages

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

Add the shadcn/ui component

npx shadcn@latest add progress
pnpm dlx shadcn@latest add progress
yarn dlx shadcn@latest add progress
bun x shadcn@latest add progress

Animate it

The CLI finds your shadcn/ui components through components.json, swaps their primitive imports for Animate UI's and removes the CSS animation classes Motion replaces.

npx @animate-ui/cli@latest animate progress
pnpm dlx @animate-ui/cli@latest animate progress
yarn dlx @animate-ui/cli@latest animate progress
bun x @animate-ui/cli@latest animate progress

Usage

Import from Animate UI instead of react-aria-components: it re-exports all of React Aria, with an animated ProgressBar. Everything else stays the same.

import { Label, ProgressBar } from '@animate-ui/react/primitives/react-aria';

export function Upload({ value }: { value: number }) {
  return (
    <ProgressBar value={value}>
      {({ percentage, valueText }) => (
        <>
          <Label>Uploading</Label>
          <span>{valueText}</span>
          <div className="h-2 overflow-hidden rounded-full bg-muted">
            <div
              className="h-full bg-primary"
              style={{ width: `${percentage}%` }}
            />
          </div>
        </>
      )}
    </ProgressBar>
  );
}

Animation

The motion of Animate UI v1's Base UI progress (v1 had no React Aria one):

  • Bar: React Aria only hands its children a percentage, and the bar is your markup, sized with an inline percentage width (style={{ width: `${percentage}%` }}, as shadcn/ui and React Aria's docs do). The ProgressBar finds that element and springs its width from the previous value to the new one (stiffness 300, damping 35: most of the way in ~230ms and settled by ~550ms, with no jump at the end; no overshoot) instead of letting it jump.
  • It starts at its value on mount, with no animation.
  • The value text is your markup: it changes at once (v1's Base UI value counted; React Aria's valueText isn't animated).
  • shadcn/ui's transition-all on the bar would run on top of Motion: the CLI removes it.
  • Under reduced motion, the bar jumps to each value.

API Reference

Every component takes exactly the props of React Aria's ProgressBar. Animate UI adds the animation props below.

ProgressBar

React Aria props, plus:

PropTypeDefault
transition?
Transition{ type: 'spring', stiffness: 300, damping: 35, restDelta: 0.01, restSpeed: 0.1 }

The bar is any element inside the ProgressBar with an inline percentage width and no child elements. AnimateUIProvider's tempo scales these transitions with everything else.

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

Last updated: 10/18/2026

On this page