Progress

A progress bar shows how far a task has got, or that it is still working that out.

x
Uploading
x
Complete
x
Loading
x
import { Progress } from "@/components/ui/progress";

export default function ProgressDemo() {
  return (
    <div className="flex w-full max-w-sm flex-col gap-6">
      <Progress value={25} aria-label="Storage used" />
      <Progress value={60} label="Uploading" showValue />
      <Progress value={100} label="Complete" showValue />
      <Progress value={null} label="Loading" />
    </div>
  );
}

The code

The component is a single file, read from the repository when the site is built. Press Show code to see all of it.

import type { ComponentProps } from "react";
import { Progress as BaseProgress } from "@base-ui/react/progress";
import { cx } from "@/lib/cx";

// Pass a number for a determinate bar, or `null` for one that is still working it out: the
// indicator then slides across the track. Reduced motion gets a still bar.
export function Progress({
  label,
  showValue = false,
  className,
  ...props
}: Omit<ComponentProps<typeof BaseProgress.Root>, "className" | "children"> & {
  label?: string;
  showValue?: boolean;
  className?: string;
}) {
  return (
    <BaseProgress.Root className={cx("flex w-full flex-col gap-2", className)} {...props}>
      {(label || showValue) && (
        <div className="flex items-center justify-between text-base">
          {label && <BaseProgress.Label className="font-medium text-text">{label}</BaseProgress.Label>}
          {showValue && (
            <BaseProgress.Value className="ml-auto text-text-secondary tabular-nums" />
          )}
        </div>
      )}
      <BaseProgress.Track className="h-2 w-full overflow-hidden rounded-full bg-border-strong">
        <BaseProgress.Indicator className="h-full rounded-full bg-accent transition-[width] duration-500 ease-out data-[indeterminate]:w-1/3 data-[indeterminate]:animate-[progress-indeterminate_1.4s_ease-in-out_infinite] motion-reduce:animate-none" />
      </BaseProgress.Track>
    </BaseProgress.Root>
  );
}

Props

PropTypeDefaultWhat it does
valuenumber | null—How far along it is, from 0 to 100. null means it doesn't know yet.
labelstring—A visible name, shown above the bar on the left.
showValuebooleanfalseShows the value as a percentage above the bar on the right.
classNamestring—Extra classes, added after the progress bar's own.

Every other prop, such as min, max and aria-label, goes to the Base UI Progress underneath.

The thinking

A progress bar is a promise about time, so it should never promise more than it knows.

  • Built on Base UI: It exposes the progressbar role and its current value for assistive tech, and formats the percentage for you.
  • Unknown is a state: Pass null when you can't tell how long it will take. The bar slides across instead of showing a number that would be a guess.

Interface design

  • Track: 8px tall and fully rounded, in the strong border color. The fill is the accent, 7.7:1 against the track.
  • Label and value: Above the bar. The label is on the left in the main text color, the value on the right in the secondary color, with numerals of equal width so it doesn't jitter.
  • Indeterminate: A bar one third of the track's width, sliding across in 1.4 seconds, over and over.

Interaction

  • Not a control: A progress bar has no hover, focus or pressed state.
  • Changing value: When the value changes, the fill grows or shrinks over 500ms.
  • Reduced motion: For people who ask their system for less motion, the indeterminate bar stays still.

Empathy

  • Announced: Assistive tech reads the label and the current value. An indeterminate bar reports no value.
  • Name it: Give every bar a name, either the visible label or an aria-label, as the first bar in the demo does.
  • Numbers help: The fill shows progress by length alone. Turn on showValue when the exact figure matters.