Agents (llms.txt)
Octocat

Progress

A determinate progress indicator for known-completion tasks. Linear and circular variants.

import { Progress } from "@/components/progress";

export default function ProgressExample() {
  return (
    <div className="w-64">
      <Progress value={60} />
    </div>
  );
}

Source Code

import { cn, cva } from "@/lib/utils/classnames";

type ProgressSize = "xs" | "sm" | "md" | "lg";
type ProgressVariant = "linear" | "circular";

const linearTrackStyle = cva({
  base: ["relative w-full overflow-hidden rounded-full", "bg-foreground/10 text-accent"],
  variants: {
    size: {
      xs: "h-0.5",
      sm: "h-1",
      md: "h-1.5",
      lg: "h-2",
    } satisfies Record<ProgressSize, string>,
  },
});

const linearFillStyle = "h-full rounded-full bg-current transition-[width] duration-150 ease-out";

const circularSizes = {
  xs: { px: 12, stroke: 1.5 },
  sm: { px: 16, stroke: 2 },
  md: { px: 20, stroke: 2 },
  lg: { px: 24, stroke: 2.5 },
} satisfies Record<ProgressSize, { px: number; stroke: number }>;

interface ProgressProps extends Omit<React.ComponentPropsWithRef<"div">, "children"> {
  /** Current value. Clamped to `[0, max]`. */
  value: number;
  /** Maximum value. Defaults to 100. */
  max?: number;
  size?: ProgressSize;
  variant?: ProgressVariant;
}

const Progress = ({
  value,
  max = 100,
  size = "md",
  variant = "linear",
  className,
  ...props
}: ProgressProps) => {
  const clamped = Math.max(0, Math.min(max, value));
  const percent = (clamped / max) * 100;

  const a11yProps = {
    role: "progressbar" as const,
    "aria-valuemin": 0,
    "aria-valuemax": max,
    "aria-valuenow": clamped,
  };

  if (variant === "circular") {
    const { px, stroke } = circularSizes[size];
    const r = (px - stroke) / 2;
    const c = 2 * Math.PI * r;
    const offset = c * (1 - percent / 100);

    return (
      <div className={cn("inline-flex text-accent", className)} {...a11yProps} {...props}>
        <svg width={px} height={px} viewBox={`0 0 ${px} ${px}`} aria-hidden="true">
          <circle
            cx={px / 2}
            cy={px / 2}
            r={r}
            fill="none"
            strokeWidth={stroke}
            className="stroke-foreground/10"
          />
          <circle
            cx={px / 2}
            cy={px / 2}
            r={r}
            fill="none"
            strokeWidth={stroke}
            strokeDasharray={c}
            strokeDashoffset={offset}
            strokeLinecap="round"
            transform={`rotate(-90 ${px / 2} ${px / 2})`}
            className="stroke-current transition-[stroke-dashoffset] duration-150 ease-out"
          />
        </svg>
      </div>
    );
  }

  return (
    <div className={linearTrackStyle({ size, className })} {...a11yProps} {...props}>
      <div className={linearFillStyle} style={{ width: `${percent}%` }} aria-hidden="true" />
    </div>
  );
};

export type { ProgressProps };
export { Progress };

API Reference

Extends the div element.

PropDefaultTypeDescription
value-numberThe current value. Clamped to `[0, max]`.
max100numberThe maximum value. Defaults to 100.
variant"linear""linear""circular"
size"md""xs""sm""md""lg"

Variants

linear
circular
import { Progress } from "@/components/progress";

export default function ProgressVariantsExample() {
  return (
    <div className="flex w-64 flex-col items-center gap-8">
      <div className="flex w-full flex-col items-center gap-2">
        <Progress value={70} />
        <span className="text-foreground-secondary text-xs">linear</span>
      </div>
      <div className="flex flex-col items-center gap-2">
        <Progress value={70} variant="circular" />
        <span className="text-foreground-secondary text-xs">circular</span>
      </div>
    </div>
  );
}

Sizes

import { Progress } from "@/components/progress";

const sizes = ["xs", "sm", "md", "lg"] as const;

export default function ProgressSizesExample() {
  return (
    <div className="grid grid-cols-[1fr_auto] items-center gap-x-8 gap-y-4">
      {sizes.map((size) => (
        <div key={size} className="contents">
          <div className="w-64">
            <Progress value={60} size={size} />
          </div>
          <Progress value={60} size={size} variant="circular" />
        </div>
      ))}
    </div>
  );
}

Color

The fill color follows currentColor. Set a text color utility on the root to retint:

          <Progress value={50} className="text-error" />
        

This makes things like an error state in a file upload trivial — no extra prop needed.

Best Practices

  1. Use Progress when you can report progress. When the task length is known and reportable (uploads, multi-step workflows, downloads), Progress communicates more than a Spinner. When all you can say is “something is happening”, reach for Spinner instead — Progress is the determinate primitive, Spinner is the indeterminate one.
  2. Accessibility:
    • Renders role="progressbar" with aria-valuemin, aria-valuemax, and aria-valuenow.
    • Provide aria-label (or aria-labelledby) when the surrounding context doesn’t already label the bar.

Previous

Portal

Next

Radio