docs / core / progress

Progress

a bar that fills to show how far along something is, or shimmers when the end is unknown. it renders no text, so it always needs a label.

intensity · recedesrsc · server

provenance

authored in usva

layer

core / primitives

intensity

recedes · safe anywhere, including dense surfaces

composition

uploads, installs, quota rows inside Card or a table cellpair with a text line that says what is progressingnot a meter for static values. it implies something is underwaynot a page loader. indeterminate work that owns the view gets Spinner

a11y

role="progressbar" with aria-valuenow/min/max, valuenow omitted when indeterminate · bring an aria-label · motion stops under reduced motionjest-axe

dependencies

class-variance-authority
live demo · try it out
customize
value0 to 100; ignored when indeterminate
55
sizetrack height
glowaccent halo on the fill
indeterminatedrop the value for the shimmer
usagecopy
import { Progress } from "usva/primitives/progress";

<Progress value={55} aria-label="Upload" />

props

proptypedefaultnotes
valuenumbercurrent progress, clamped to [0, max]. omit it for the indeterminate shimmer.
maxnumber100upper bound for value.
size"sm" | "md" | "lg""md"track height, 4 to 12 pixels. pick by the density of the row it sits in.
glowbooleanfalsethe accent halo on the fill. one glowing bar per view.

get it

npx shadcn add https://usva.build/r/progress.jsoncopy
source · components/ui/progress.tsxexactly what this command copiescopy
import { cva, type VariantProps } from "class-variance-authority";
import * as React from "react";
import { cn } from "@/lib/utils";

export const progressVariants = cva(
  "w-full overflow-hidden rounded-full bg-border-strong",
  {
    variants: {
      size: {
        sm: "h-1",
        md: "h-2",
        lg: "h-3",
      },
    },
    defaultVariants: { size: "md" },
  },
);

export interface ProgressProps
  extends Omit<React.HTMLAttributes<HTMLDivElement>, "children">,
    VariantProps<typeof progressVariants> {
  value?: number;
  max?: number;
  glow?: boolean;
}

export const Progress = React.forwardRef<HTMLDivElement, ProgressProps>(
  ({ className, size, value, max = 100, glow = false, ...props }, ref) => {
    const indeterminate = value === undefined;
    const pct = indeterminate
      ? 0
      : Math.max(0, Math.min(100, (value / max) * 100));
    const complete = !indeterminate && pct >= 100;

    return (
      <div
        ref={ref}
        role="progressbar"
        aria-valuemin={0}
        aria-valuemax={max}
        aria-valuenow={indeterminate ? undefined : value}
        className={cn(progressVariants({ size }), className)}
        {...props}
      >
        <div
          className={cn(
            "h-full rounded-full",
            complete ? "bg-live" : "bg-accent bg-gradient-accent",
            glow && "glow-accent",
            indeterminate
              ? "w-full animate-shimmer motion-reduce:animate-none"
              : "transition-layout duration-ambient ease-soft motion-reduce:transition-none",
          )}
          style={indeterminate ? undefined : { width: `${pct}%` }}
        />
      </div>
    );
  },
);
Progress.displayName = "Progress";