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.

Prop Default Type Description
value - number The current value. Clamped to `[0, max]`.
max 100 number The 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