Progress Bar

Non-interactive progress indicator. Communicates completion, health, or utilization through a filled track. Four semantic variants, three sizes, and an indeterminate shimmer mode.

Preview

Default (65%)65%
Success (100%)100%
Warning (80%)80%
Danger (95%)95%

Sizes

Three track heights for different information densities. Use sm inside dense table rows and list items; lg for prominent hero metrics.

sm
4px
md
6px
lg
8px

Indeterminate

When end value is unknown — file upload, background job, AI generation — show a shimmer sweep instead of a static fill. Set aria-busy="true" and omit aria-valuenow.

Tokens

TokenDarkLightUse
--progress-trackrgb(40, 42, 48)rgb(209, 213, 219)Track background
--progress-success#22c55e#22c55eSuccess fill
--progress-warning#f59e0b#f59e0bWarning fill
--progress-danger#ef4444#ef4444Danger fill

Implementation

ProgressBar.tsx
tsx
import { cn } from "@/lib";

interface ProgressBarProps {
  value?: number;
  max?: number;
  variant?: "default" | "success" | "warning" | "danger";
  size?: "sm" | "md" | "lg";
  indeterminate?: boolean;
  showValue?: boolean;
  label?: string;
  className?: string;
}

const fillColors = {
  default: "rgb(var(--accent))",
  success: "rgb(var(--progress-success))",
  warning: "rgb(var(--progress-warning))",
  danger:  "rgb(var(--progress-danger))",
};

const heights = { sm: 4, md: 6, lg: 8 };

export function ProgressBar({
  value = 0,
  max = 100,
  variant = "default",
  size = "md",
  indeterminate = false,
  showValue = false,
  label,
  className,
}: ProgressBarProps) {
  const pct = Math.min(100, Math.max(0, (value / max) * 100));
  const h = heights[size];

  return (
    <div className={cn("w-full", className)}>
      {showValue && (
        <div className="flex justify-end mb-1">
          <span className="text-[11px] font-medium text-[rgb(var(--text-tertiary))]">
            {Math.round(pct)}%
          </span>
        </div>
      )}
      <div
        role="progressbar"
        aria-valuenow={indeterminate ? undefined : value}
        aria-valuemin={0}
        aria-valuemax={max}
        aria-label={label}
        aria-busy={indeterminate}
        className="relative overflow-hidden rounded-full"
        style={{
          height: h,
          background: "rgb(var(--progress-track))",
          boxShadow: "inset 0 1px 0 rgba(0,0,0,0.12)",
        }}
      >
        {/* Specular top-edge highlight */}
        <div
          className="absolute inset-x-0 top-0 pointer-events-none"
          style={{
            height: 1,
            background: "linear-gradient(90deg, transparent, rgba(255,255,255,0.18), transparent)",
          }}
        />
        {indeterminate ? (
          <div
            className="absolute inset-y-0 w-1/3 rounded-full animate-[shimmer_1.4s_ease-in-out_infinite]"
            style={{ background: fillColors[variant] }}
          />
        ) : (
          <div
            className="h-full rounded-full transition-[width] duration-500 ease-out"
            style={{ width: `${pct}%`, background: fillColors[variant] }}
          />
        )}
      </div>
    </div>
  );
}

Props

PropTypeDefaultDescription
valuenumberCurrent progress value between 0 and max.
maxnumber100Maximum value. Percentage is computed as value / max.
variant"default" | "success" | "warning" | "danger""default"Semantic fill colour — maps to the corresponding status token.
size"sm" | "md" | "lg""md"Track height: sm = 4 px, md = 6 px, lg = 8 px.
indeterminatebooleanfalseShows a shimmer sweep animation when the end value is unknown.
showValuebooleanfalseRenders a percentage label above the right edge of the track.
labelstringAccessible label attached via aria-label. Required for screen readers.

Accessibility

  • Use role="progressbar" with aria-valuenow, aria-valuemin, and aria-valuemax on determinate bars.
  • For indeterminate bars, set aria-busy="true" and omit aria-valuenow — do not set it to 0.
  • Always provide an aria-label or a visible text label linked with aria-labelledby.
  • Colour alone does not convey variant meaning — pair the variant with a label (e.g., "Budget: 95% — Critical").
  • Progress bars are non-interactive and never receive keyboard focus.