Skip to content

Progress

Shows how far a task has come, or that it is underway when its length is unknown.

SourceBase UIRegistry
Uploading assets
x
import {
  Progress,
  ProgressLabel,
  ProgressValue,
} from "@/components/ui/progress"

export default function ProgressDemo() {
  return (
    <Progress value={64} className="w-full max-w-xs">
      <ProgressLabel>Uploading assets</ProgressLabel>
      <ProgressValue />
    </Progress>
  )
}

Anatomy

Uploading assets
x
  1. 1LabelSays what is in progress. It also names the bar for screen readers.
  2. 2TrackThe whole task, in a muted tint.
  3. 3ValueThe percentage, in tabular figures so it doesn't jitter as it changes.
  4. 4IndicatorThe completed share. It animates between values, and sweeps across the track when the value is unknown.

Installation

$ pnpm dlx shadcn@latest add https://www.prfct.dev/r/progress.json

Usage

import { Progress, ProgressLabel, ProgressValue } from "@/components/ui/progress"
<Progress value={64}>
  <ProgressLabel>Uploading assets</ProgressLabel>
  <ProgressValue />
</Progress>

Progress renders the track and indicator for you. Children — typically a label and a value — sit on the line above the bar.

Examples

Sizes

sm (4px) fits inside lists and table cells, default (6px) suits most forms and cards, and lg (10px) is for a progress bar that is the focus of the view.

Small
x
Default
x
Large
x
import {
  Progress,
  ProgressLabel,
  ProgressValue,
} from "@/components/ui/progress"

export default function ProgressSizes() {
  return (
    <div className="flex w-full max-w-sm flex-col gap-6">
      <Progress value={30} size="sm">
        <ProgressLabel>Small</ProgressLabel>
        <ProgressValue />
      </Progress>
      <Progress value={55}>
        <ProgressLabel>Default</ProgressLabel>
        <ProgressValue />
      </Progress>
      <Progress value={80} size="lg">
        <ProgressLabel>Large</ProgressLabel>
        <ProgressValue />
      </Progress>
    </div>
  )
}

Indeterminate

Pass value={null} when you know work is happening but not how much remains. A segment sweeps across the track until you switch to a determinate value.

Preparing your export…
x
import { Progress, ProgressLabel } from "@/components/ui/progress"

export default function ProgressIndeterminate() {
  return (
    <Progress value={null} className="w-full max-w-xs">
      <ProgressLabel>Preparing your export…</ProgressLabel>
    </Progress>
  )
}

Live updates

The indicator animates between values with the slow duration and standard easing, so frequent updates read as smooth motion rather than jumps.

Building…
x
"use client"

import * as React from "react"
import { RotateCcwIcon } from "lucide-react"

import { Button } from "@/components/ui/button"
import {
  Progress,
  ProgressLabel,
  ProgressValue,
} from "@/components/ui/progress"

export default function ProgressLive() {
  const [value, setValue] = React.useState(0)
  const [run, setRun] = React.useState(0)

  React.useEffect(() => {
    // Each run counts up from zero; Restart resets the value and starts a new run.
    let progress = 0
    const timer = setInterval(() => {
      progress = Math.min(100, progress + Math.round(Math.random() * 12 + 4))
      setValue(progress)
      if (progress === 100) clearInterval(timer)
    }, 450)
    return () => clearInterval(timer)
  }, [run])

  return (
    <div className="flex w-full max-w-xs flex-col items-end gap-4">
      <Progress value={value} className="w-full">
        <ProgressLabel>
          {value === 100 ? "Build complete" : "Building…"}
        </ProgressLabel>
        <ProgressValue />
      </Progress>
      <Button
        variant="outline"
        size="xs"
        onClick={() => {
          setValue(0)
          setRun((n) => n + 1)
        }}
      >
        <RotateCcwIcon data-icon="inline-start" />
        Restart
      </Button>
    </div>
  )
}

File uploads

In lists, use the sm size and let the file name be the label. Swap the value for a confirmation when an item finishes, so completed rows stop drawing the eye.

  • brand-guidelines.pdfDone4.2 MB
    x
  • hero-illustration.png2.8 MB
    x
  • product-walkthrough.mp4148 MB
    x
import {
  CircleCheckIcon,
  FileTextIcon,
  ImageIcon,
  FileVideoIcon,
} from "lucide-react"

import {
  Progress,
  ProgressLabel,
  ProgressValue,
} from "@/components/ui/progress"

const files = [
  {
    name: "brand-guidelines.pdf",
    size: "4.2 MB",
    value: 100,
    icon: FileTextIcon,
  },
  { name: "hero-illustration.png", size: "2.8 MB", value: 72, icon: ImageIcon },
  {
    name: "product-walkthrough.mp4",
    size: "148 MB",
    value: 18,
    icon: FileVideoIcon,
  },
]

export default function ProgressFileUpload() {
  return (
    <ul className="flex w-full max-w-md flex-col divide-y rounded-xl border bg-card shadow-raised">
      {files.map((file) => {
        const Icon = file.icon
        const done = file.value === 100
        return (
          <li key={file.name} className="flex items-start gap-3 p-4">
            <span className="flex size-9 shrink-0 items-center justify-center rounded-lg border bg-surface text-muted-foreground">
              <Icon className="size-4" />
            </span>
            <Progress value={file.value} size="sm" className="min-w-0 flex-1">
              <ProgressLabel className="truncate">{file.name}</ProgressLabel>
              {done ? (
                <span className="ml-auto flex items-center gap-1 text-sm text-success-11">
                  <CircleCheckIcon className="size-4" />
                  Done
                </span>
              ) : (
                <ProgressValue />
              )}
              <span className="order-last w-full text-xs text-muted-foreground">
                {file.size}
              </span>
            </Progress>
          </li>
        )
      })}
    </ul>
  )
}

Custom value

ProgressValue accepts a render function for units other than percent. When you change the visible format, give assistive technology the same words with getAriaValueText.

Storage
x
"use client"

import {
  Progress,
  ProgressLabel,
  ProgressValue,
} from "@/components/ui/progress"

const used = 7.2
const total = 10
const format = (value: number) => value.toLocaleString("en-US")

export default function ProgressCustomValue() {
  return (
    <Progress
      value={used}
      max={total}
      getAriaValueText={() =>
        `${format(used)} of ${format(total)} gigabytes used`
      }
      className="w-full max-w-xs"
    >
      <ProgressLabel>Storage</ProgressLabel>
      <ProgressValue>
        {() => `${format(used)} GB of ${format(total)} GB`}
      </ProgressValue>
    </Progress>
  )
}
<Progress
  value={7.2}
  max={10}
  getAriaValueText={() => "7.2 of 10 gigabytes used"}
>
  <ProgressLabel>Storage</ProgressLabel>
  <ProgressValue>{() => "7.2 GB of 10 GB"}</ProgressValue>
</Progress>

Guidelines

When to use

  • For tasks that take longer than about a second and report measurable progress: uploads, exports, imports, builds.
  • For quantities against a limit — storage, seats, quota — where the bar is a gauge rather than a timer.

When not to use

  • For short or unmeasurable waits inside a control — use a Spinner or the Button's loading state.
  • For content that is still loading — use a Skeleton that previews the layout.
  • For steps in a flow. A progress bar measures work, not navigation.

Skeleton, spinner or progress?

WaitUseWhy
Under ~1 secondNothingIndicators that flash for a moment feel slower than none.
Content is on its waySkeletonShows the shape of what's coming, so the page doesn't jump.
An action is in flightSpinner or Button loadingConfirms the system heard the request.
Measurable work, over ~1 secondProgressTells people how long they'll wait, so they can decide to stay.

Be honest

Never let a determinate bar stall at 99% or jump backward. If you can't estimate progress reliably, use the indeterminate state and say what's happening in the label.

Importing 1,280 contacts
x
DoThe label says what is happening, the value says how far along it is.
x
Don’tA bar without a label forces people to guess what is loading.

Accessibility

  • Renders role="progressbar" with aria-valuemin, aria-valuemax and aria-valuenow. The indeterminate state omits aria-valuenow, which screen readers announce as busy.
  • ProgressLabel is wired to the bar with aria-labelledby. When you can't show a label, pass aria-label to Progress instead — a progress bar must always have a name.
  • aria-valuetext is generated from the formatted value. Override it with getAriaValueText whenever the visible value isn't a percentage.
  • Progress bars aren't live regions: screen readers read the value when people navigate to it, not on every change. Announce milestones — Upload complete — with a Toast or a role="status" message.
  • Under prefers-reduced-motion, value changes and the indeterminate sweep stop animating.

API reference

Progress

The root. Renders a <div> containing your children followed by ProgressTrack and ProgressIndicator. Accepts every prop of Base UI's Progress.Root.

PropTypeDefault
valuerequired

The current value. null renders the indeterminate state.

number | nullNo default
min

The minimum value.

number0
max

The maximum value.

number100
size

Track height: 4px, 6px or 10px.

"sm" | "default" | "lg""default"
format

Formats the value shown by ProgressValue and announced to screen readers.

Intl.NumberFormatOptionsNo default
locale

Locale for number formatting. Defaults to the user's runtime locale.

Intl.LocalesArgumentNo default
getAriaValueText

Returns a human-readable alternative for the current value.

(formattedValue: string, value: number | null) => stringNo default

ProgressLabel

The accessible name of the bar. Renders a <span>.

PropTypeDefault
className

Additional classes.

stringNo default

ProgressValue

Displays the formatted value, right-aligned. Renders a <span>.

PropTypeDefault
children

Custom rendering for the value, e.g. units other than percent.

(formattedValue: string | null, value: number | null) => ReactNodeNo default

ProgressTrack and ProgressIndicator

Rendered by Progress automatically. They are exported for custom compositions — for example, a segmented meter that renders several indicators.