Progress

Displays the status of a task that takes a long time.

x
"use client"

import * as React from "react"
import { Progress as ProgressPrimitive } from "@animate-ui/react/primitives/base-ui/progress"
import { cn } from "cn"

function Progress({
  className,
  children,
  value,
  ...props
}: ProgressPrimitive.Root.Props) {
  return (
    <ProgressPrimitive.Root
      value={value}
      data-slot="progress"
      className={cn("flex flex-wrap gap-3", className)}
      {...props}
    >
      {children}
      <ProgressTrack>
        <ProgressIndicator />
      </ProgressTrack>
    </ProgressPrimitive.Root>
  )
}

function ProgressTrack({ className, ...props }: ProgressPrimitive.Track.Props) {
  return (
    <ProgressPrimitive.Track
      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,
  ...props
}: ProgressPrimitive.Indicator.Props) {
  return (
    <ProgressPrimitive.Indicator
      data-slot="progress-indicator"
      className={cn("bg-primary h-full", className)}
      {...props}
    />
  )
}

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

function ProgressValue({ className, ...props }: ProgressPrimitive.Value.Props) {
  return (
    <ProgressPrimitive.Value
      className={cn("text-muted-foreground ml-auto text-sm tabular-nums", className)}
      data-slot="progress-value"
      {...props}
    />
  )
}

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 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 the primitive from Animate UI instead of Base UI. Everything else stays the same.

import { Progress } from '@animate-ui/react/primitives/base-ui/progress';

export function Upload({ value }: { value: number }) {
  return (
    <Progress.Root value={value}>
      <Progress.Label>Uploading</Progress.Label>
      <Progress.Value />
      <Progress.Track className="h-2 overflow-hidden rounded-full">
        <Progress.Indicator className="bg-primary" />
      </Progress.Track>
    </Progress.Root>
  );
}

Using the @base-ui/react umbrella import instead? The barrel mirrors it: import { Progress } from '@animate-ui/react/primitives/base-ui'.

Animation

The motion of Animate UI v1's progress:

  • Indicator: Base UI sizes it with a percentage width, and its width springs to each new value (stiffness 300, damping 35: most of the way in ~230ms and settled by ~550ms, with no jump at the end; no overshoot), the width v1 animated. Right-to-left works as Base UI lays it out (inset-inline-start).
  • Value counts to each new value on the bar's own spring (stiffness 300, damping 35), so the number moves with the bar, formatted as Base UI formats it (a percentage, or your format and locale, within min and max), from min (0 by default) when it appears, as v1's counting value did. A children function gets the counted text and number.
  • The indicator starts at its value on mount, with no animation.
  • Indeterminate (value={null}): the indicator is left to your styles, and the Value to Base UI.
  • shadcn/ui's transition-all on the indicator would run on top of Motion: the CLI removes it.
  • Under reduced motion, the indicator jumps to each value (the Value still counts).
  • Without a locale, Base UI formats the numbers (the Value, aria-valuetext) in the runtime's locale: the server's on the server and the browser's in the browser, so a page rendered in English and opened in another language didn't hydrate cleanly ("13%" against "13 %"). Animate UI formats them in en-US on the server and while the page hydrates, then in the browser's locale, as React Aria does. Pass locale to pick one yourself.

API Reference

Every part takes exactly the props of Base UI's Progress. Animate UI adds the animation props below.

Root, Track, Label

Base UI props only.

Indicator

Base UI props, plus:

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

Value

Base UI props, plus:

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

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