---
title: Progress
description: A completion bar with an optional label and live value.
group: Components
order: 53
---

Show completion with a plain bar, a labeled live value, or a value driven by another control.

```tsx
"use client"

import * as React from "react"
import { Button } from "@/components/ui/button"
import { Progress } from "@/components/ui/progress"

const DURATION_MS = 2800

export function Example() {
  const [run, setRun] = React.useState(0)
  const progress = useProgressRun(run)

  return (
    <div className="relative w-full">
      <Button
        type="button"
        variant="ghost"
        size="icon-sm"
        className="absolute top-0 right-0 z-20"
        aria-label="Reload"
        onClick={() => setRun((n) => n + 1)}
      >
        <ReloadIcon />
      </Button>
      <Progress
        value={progress}
        className="w-full [&_[data-slot=progress-indicator]]:duration-0"
      />
    </div>
  )
}

function useProgressRun(run: number) {
  const [progress, setProgress] = React.useState(0)

  React.useEffect(() => {
    setProgress(0)
    const start = performance.now()
    let frame = 0

    function tick(now: number) {
      const t = Math.min(1, (now - start) / DURATION_MS)
      setProgress(Math.round(t * 100))
      if (t < 1) frame = requestAnimationFrame(tick)
    }

    frame = requestAnimationFrame(tick)
    return () => cancelAnimationFrame(frame)
  }, [run])

  return progress
}

function ReloadIcon() {
  return (
    <svg
      xmlns="http://www.w3.org/2000/svg"
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth="2"
      strokeLinecap="round"
      strokeLinejoin="round"
      className="size-3.5"
      aria-hidden
    >
      <path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8" />
      <path d="M21 3v5h-5" />
      <path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16" />
      <path d="M8 16H3v5" />
    </svg>
  )
}
```

## Installation

```bash
npx @arctis-sh/@arctis-sh/ui@latest add progress
```

## Usage

```tsx
import { Progress } from "@/components/ui/progress"
```

```tsx
<Progress value={33} />
```

## Composition

```tree
Progress
├── ProgressLabel
├── ProgressValue
└── ProgressTrack
    └── ProgressIndicator
```

`ProgressTrack` and `ProgressIndicator` are included automatically. Add `ProgressLabel` and `ProgressValue` when you want a header row.

## Basic

A plain bar from 0 to 100. Reload the example to replay the fill animation.

```tsx
"use client"

import * as React from "react"
import { Button } from "@/components/ui/button"
import { Progress } from "@/components/ui/progress"

const DURATION_MS = 2800

export function Example() {
  const [run, setRun] = React.useState(0)
  const progress = useProgressRun(run)

  return (
    <div className="relative w-full">
      <Button
        type="button"
        variant="ghost"
        size="icon-sm"
        className="absolute top-0 right-0 z-20"
        aria-label="Reload"
        onClick={() => setRun((n) => n + 1)}
      >
        <ReloadIcon />
      </Button>
      <Progress
        value={progress}
        className="w-full [&_[data-slot=progress-indicator]]:duration-0"
      />
    </div>
  )
}

function useProgressRun(run: number) {
  const [progress, setProgress] = React.useState(0)

  React.useEffect(() => {
    setProgress(0)
    const start = performance.now()
    let frame = 0

    function tick(now: number) {
      const t = Math.min(1, (now - start) / DURATION_MS)
      setProgress(Math.round(t * 100))
      if (t < 1) frame = requestAnimationFrame(tick)
    }

    frame = requestAnimationFrame(tick)
    return () => cancelAnimationFrame(frame)
  }, [run])

  return progress
}

function ReloadIcon() {
  return (
    <svg
      xmlns="http://www.w3.org/2000/svg"
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth="2"
      strokeLinecap="round"
      strokeLinejoin="round"
      className="size-3.5"
      aria-hidden
    >
      <path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8" />
      <path d="M21 3v5h-5" />
      <path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16" />
      <path d="M8 16H3v5" />
    </svg>
  )
}
```

## Label

Add `ProgressLabel` and `ProgressValue` for a title and live percentage.

```tsx
"use client"

import * as React from "react"
import { Button } from "@/components/ui/button"
import {
  Progress,
  ProgressLabel,
  ProgressValue,
} from "@/components/ui/progress"

const DURATION_MS = 2800

export function Example() {
  const [run, setRun] = React.useState(0)
  const progress = useProgressRun(run)

  return (
    <div className="relative w-full">
      <Button
        type="button"
        variant="ghost"
        size="icon-sm"
        className="absolute top-0 right-0 z-20"
        aria-label="Reload"
        onClick={() => setRun((n) => n + 1)}
      >
        <ReloadIcon />
      </Button>
      <Progress
        value={progress}
        className="w-full [&_[data-slot=progress-indicator]]:duration-0"
      >
        <ProgressLabel>Upload progress</ProgressLabel>
        <ProgressValue />
      </Progress>
    </div>
  )
}

function useProgressRun(run: number) {
  const [progress, setProgress] = React.useState(0)

  React.useEffect(() => {
    setProgress(0)
    const start = performance.now()
    let frame = 0

    function tick(now: number) {
      const t = Math.min(1, (now - start) / DURATION_MS)
      setProgress(Math.round(t * 100))
      if (t < 1) frame = requestAnimationFrame(tick)
    }

    frame = requestAnimationFrame(tick)
    return () => cancelAnimationFrame(frame)
  }, [run])

  return progress
}

function ReloadIcon() {
  return (
    <svg
      xmlns="http://www.w3.org/2000/svg"
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth="2"
      strokeLinecap="round"
      strokeLinejoin="round"
      className="size-3.5"
      aria-hidden
    >
      <path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8" />
      <path d="M21 3v5h-5" />
      <path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16" />
      <path d="M8 16H3v5" />
    </svg>
  )
}
```

## Controlled

Drive the bar from another control. This example uses a `Slider`.

```tsx
"use client"

import * as React from "react"
import {
  Progress,
  ProgressLabel,
  ProgressValue,
} from "@/components/ui/progress"
import { Slider } from "@/components/ui/slider"

export function Example() {
  const [value, setValue] = React.useState([40])

  return (
    <div className="grid w-full gap-4">
      <Progress
        value={value[0] ?? 0}
        className="w-full [&_[data-slot=progress-indicator]]:duration-0"
      >
        <ProgressLabel>Export</ProgressLabel>
        <ProgressValue />
      </Progress>
      <Slider
        value={value}
        onValueChange={(next) => {
          const nextValue = readSliderValue(next)
          setValue((prev) => (prev[0] === nextValue ? prev : [nextValue]))
        }}
        min={0}
        max={100}
        step={1}
      />
    </div>
  )
}

function readSliderValue(next: number | readonly number[]) {
  if (typeof next === "number") return next
  if (Array.isArray(next)) return next[0] ?? 0
  return 0
}
```

## API Reference

### Progress

`value` is clamped between `0` and `max`.

| Prop | Type | Default |
| --- | --- | --- |
| value | number | 0 |
| max | number | 100 |
| className | string | — |

```tsx
<Progress value={60} max={100} />
```

### ProgressLabel

Sets `aria-labelledby` on the progressbar when present.

| Prop | Type | Default |
| --- | --- | --- |
| className | string | — |

```tsx
<Progress>
  <ProgressLabel>Uploading</ProgressLabel>
  <ProgressValue />
</Progress>
```

### ProgressValue

Defaults to a rounded percentage. Pass children or a render function to customize.

| Prop | Type | Default |
| --- | --- | --- |
| children | ReactNode \| ((value: number, max: number) => ReactNode) | — |
| className | string | — |

```tsx
<ProgressValue />
```

### ProgressTrack

Optional. Rendered automatically when omitted.

| Prop | Type | Default |
| --- | --- | --- |
| className | string | — |

```tsx
<ProgressTrack>
  <ProgressIndicator />
</ProgressTrack>
```

### ProgressIndicator

Optional. Rendered automatically inside the track when omitted. Width follows `value` / `max`. Defaults to a `duration-300` width transition — override with `className` when you want instant updates.

| Prop | Type | Default |
| --- | --- | --- |
| className | string | — |

```tsx
<ProgressIndicator />
```

## Source

```tsx
"use client";

import {
  Children,
  createContext,
  isValidElement,
  useContext,
  useId,
  type HTMLAttributes,
  type ReactNode,
} from "react";
import { cn } from "@/lib/utils";

type ProgressContextValue = {
  value: number;
  max: number;
  labelId?: string;
};

const ProgressContext = createContext<ProgressContextValue | null>(null);

function useProgress() {
  const context = useContext(ProgressContext);
  if (!context) {
    throw new Error("Progress parts must be used within <Progress>");
  }
  return context;
}

function clamp(value: number, min: number, max: number) {
  return Math.min(max, Math.max(min, value));
}

function slotName(child: ReactNode) {
  if (!isValidElement(child)) return null;
  const type = child.type as { displayName?: string; name?: string };
  if (typeof type === "string") return null;
  return type.displayName ?? type.name ?? null;
}

type ProgressProps = Omit<HTMLAttributes<HTMLDivElement>, "defaultValue"> & {
  value?: number;
  max?: number;
};

function Progress({
  className,
  value = 0,
  max = 100,
  children,
  ...props
}: ProgressProps) {
  const labelId = useId();
  const safeMax = max > 0 ? max : 100;
  const safeValue = clamp(value, 0, safeMax);
  const childArray = Children.toArray(children);
  const hasChildren = childArray.length > 0;
  const hasLabel = childArray.some(
    (child) => slotName(child) === "ProgressLabel",
  );
  const hasTrack = childArray.some(
    (child) => slotName(child) === "ProgressTrack",
  );

  return (
    <ProgressContext.Provider
      value={{ value: safeValue, max: safeMax, labelId }}
    >
      <div
        role="progressbar"
        data-slot="progress"
        aria-valuemin={0}
        aria-valuemax={safeMax}
        aria-valuenow={safeValue}
        aria-labelledby={hasLabel ? labelId : undefined}
        className={cn(
          hasChildren
            ? "grid w-full grid-cols-[1fr_auto] items-center gap-x-2 gap-y-2 [&_[data-slot=progress-track]]:col-span-2"
            : "relative h-1.5 w-full",
          className,
        )}
        {...props}
      >
        {hasChildren ? children : null}
        {hasChildren && !hasTrack ? (
          <ProgressTrack>
            <ProgressIndicator />
          </ProgressTrack>
        ) : null}
        {!hasChildren ? (
          <ProgressTrack className="absolute inset-0">
            <ProgressIndicator />
          </ProgressTrack>
        ) : null}
      </div>
    </ProgressContext.Provider>
  );
}

type ProgressLabelProps = HTMLAttributes<HTMLSpanElement>;

function ProgressLabel({ className, ...props }: ProgressLabelProps) {
  const { labelId } = useProgress();

  return (
    <span
      id={labelId}
      data-slot="progress-label"
      className={cn(
        "text-sm font-medium tracking-wide text-foreground",
        className,
      )}
      {...props}
    />
  );
}
ProgressLabel.displayName = "ProgressLabel";

type ProgressValueProps = HTMLAttributes<HTMLSpanElement> & {
  children?: ReactNode | ((value: number, max: number) => ReactNode);
};

function ProgressValue({ className, children, ...props }: ProgressValueProps) {
  const { value, max } = useProgress();
  const content =
    typeof children === "function"
      ? children(value, max)
      : (children ?? `${Math.round((value / max) * 100)}%`);

  return (
    <span
      data-slot="progress-value"
      className={cn(
        "text-sm font-normal tracking-wide text-muted-foreground tabular-nums",
        className,
      )}
      {...props}
    >
      {content}
    </span>
  );
}
ProgressValue.displayName = "ProgressValue";

type ProgressTrackProps = HTMLAttributes<HTMLDivElement>;

function ProgressTrack({ className, children, ...props }: ProgressTrackProps) {
  return (
    <div
      data-slot="progress-track"
      className={cn(
        "relative h-1.5 w-full overflow-hidden rounded-md bg-input",
        className,
      )}
      {...props}
    >
      {children ?? <ProgressIndicator />}
    </div>
  );
}
ProgressTrack.displayName = "ProgressTrack";

type ProgressIndicatorProps = HTMLAttributes<HTMLDivElement>;

function ProgressIndicator({
  className,
  style,
  ...props
}: ProgressIndicatorProps) {
  const { value, max } = useProgress();
  const percent = (value / max) * 100;

  return (
    <div
      data-slot="progress-indicator"
      className={cn(
        "h-full rounded-md bg-primary transition-[width] duration-300 ease-out",
        className,
      )}
      style={{ width: `${percent}%`, ...style }}
      {...props}
    />
  );
}
ProgressIndicator.displayName = "ProgressIndicator";

export {
  Progress,
  ProgressLabel,
  ProgressValue,
  ProgressTrack,
  ProgressIndicator,
};
```
