Siteplane UI

Progress

Show how much of an operation has completed.

Markdown
x
"use client";

import { useEffect, useState } from "react";
import { Progress } from "@/siteplane/components/progress";

export default function Example() {
  const [value, setValue] = useState(0);

  // Simulates an upload: new values arrive in uneven steps, the upload rests at
  // 100 and starts again. The indicator moves evenly toward each value.
  useEffect(() => {
    const timer = window.setTimeout(
      () =>
        setValue(
          value >= 100
            ? 0
            : Math.min(100, value + 3 + Math.floor(Math.random() * 7)),
        ),
      value >= 100 ? 1600 : 400,
    );
    return () => window.clearTimeout(timer);
  }, [value]);

  return (
    <Progress aria-label="Upload progress" className="max-w-sm" value={value} />
  );
}

Installation

pnpm dlx shadcn@4.18.0 add @siteplane/progress

Complete the one-time setup first. Registry JSON lists source files and dependencies.

Usage

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

<Progress value={40} aria-label="Upload progress" />;

Composition

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

Label and value

Supplying children replaces the automatic track. Include Track and Indicator explicitly.

Upload progress
x
"use client";

import { useEffect, useState } from "react";
import {
  Progress,
  ProgressIndicator,
  ProgressLabel,
  ProgressTrack,
  ProgressValue,
} from "@/siteplane/components/progress";

export default function Example() {
  const [value, setValue] = useState(0);

  // Simulates an upload: new values arrive in uneven steps, the upload rests at
  // 100 and starts again.
  useEffect(() => {
    const timer = window.setTimeout(
      () =>
        setValue(
          value >= 100
            ? 0
            : Math.min(100, value + 3 + Math.floor(Math.random() * 7)),
        ),
      value >= 100 ? 1600 : 400,
    );
    return () => window.clearTimeout(timer);
  }, [value]);

  return (
    <Progress className="max-w-sm" value={value}>
      <div className="flex justify-between gap-4">
        <ProgressLabel>Upload progress</ProgressLabel>
        <ProgressValue />
      </div>
      <ProgressTrack>
        <ProgressIndicator />
      </ProgressTrack>
    </Progress>
  );
}

Range and formatting

Values and formatting belong to the root. This example counts megabytes instead of percentages and adds the total to the displayed value.

Uploading backup
x
"use client";

import { useEffect, useState } from "react";
import {
  Progress,
  ProgressIndicator,
  ProgressLabel,
  ProgressTrack,
  ProgressValue,
} from "@/siteplane/components/progress";

export default function Example() {
  const [value, setValue] = useState(0);

  // Simulates a backup upload that reports megabytes: it rests at 500 MB and
  // starts again.
  useEffect(() => {
    const timer = window.setTimeout(
      () =>
        setValue(
          value >= 500
            ? 0
            : Math.min(500, value + 12 + Math.floor(Math.random() * 28)),
        ),
      value >= 500 ? 1600 : 400,
    );
    return () => window.clearTimeout(timer);
  }, [value]);

  return (
    <Progress
      className="max-w-sm"
      value={value}
      max={500}
      format={{ style: "unit", unit: "megabyte" }}
    >
      <div className="flex justify-between gap-4">
        <ProgressLabel>Uploading backup</ProgressLabel>
        <ProgressValue>
          {(formattedValue) => `${formattedValue} of 500 MB`}
        </ProgressValue>
      </div>
      <ProgressTrack>
        <ProgressIndicator />
      </ProgressTrack>
    </Progress>
  );
}

Circular progress

ProgressCircle is a separate, self-contained component; it takes a value and no children.

x
x
x
x
"use client";

import { useEffect, useState } from "react";
import { ProgressCircle } from "@/siteplane/components/progress";

export default function Example() {
  const [value, setValue] = useState(0);

  // Simulates an upload: new values arrive in uneven steps, the upload rests at
  // 100 and starts again. Every circle follows the same value.
  useEffect(() => {
    const timer = window.setTimeout(
      () =>
        setValue(
          value >= 100
            ? 0
            : Math.min(100, value + 3 + Math.floor(Math.random() * 7)),
        ),
      value >= 100 ? 1600 : 400,
    );
    return () => window.clearTimeout(timer);
  }, [value]);

  return (
    <div className="flex items-center gap-4">
      {(["sm", "md", "lg", "xl"] as const).map((size) => (
        <ProgressCircle
          key={size}
          aria-label="Upload progress"
          size={size}
          value={value}
        />
      ))}
    </div>
  );
}

Smooth value changes

The indicator moves evenly toward each new value using the existing --motion-duration-linger token (380 ms by default). Incoming updates can continue an active transition. Numeric and accessible progress values reflect the actual value immediately.

The circle uses --motion-duration-slow. OS and kit reduced-motion settings disable these transitions, so the bar and the circle then jump to each new value. The application still owns the real progress; the component does not simulate uploads or generate additional progress values. The examples on this page stand in for a real upload with a timer: a new value every few hundred milliseconds, a rest at the end and a fresh start.

API reference

Component properties
PropTypeDefaultDescription
Progress.sizesm | md | lg | xlprovider / lgRoot size inherited by Label, Track and Value.
Progress.localestringen-USExplicit kit formatting default; not automatically the locale provider.
Progress.childrenReactNodeTrack with IndicatorCustom children replace the automatic track.
Label / Track / Value.sizesm | md | lg | xlroot / provider / lgOverride the part size.

ProgressLabel associates the visible label; ProgressTrack contains ProgressIndicator; ProgressValue formats the current root value; a function child receives the formatted and the raw value. Indicator has no owned props. All styled parts accept string className. Root receives value, min, max, format and accessibility labels from Base UI Progress; those numerical and formatting contracts are inherited, not separate Siteplane UI APIs. Pass value={null} for an indeterminate bar when completion is unknown; a fill then runs from the start of the track again and again. With reduced motion the bar and the circle hold still, as a segment in the middle of the track and an arc on top of the circle.

ProgressProps, ProgressLabelProps, ProgressTrackProps and ProgressValueProps export these same size extensions. ProgressPrimitive is the foundation namespace.

ProgressCircle

Component properties
PropTypeDefaultDescription
valuenumber | nullrequiredNumeric completion; non-finite values use min. null shows an indeterminate circle.
min / maxnumber0 / 100Range; visual percentage clamps to 0–100 and is zero for a non-positive range.
sizesm | md | lg | xlprovider / lgCircle sizes 20, 28, 32 and 40 CSS pixels respectively.
localestringen-USFormatting locale.

ProgressCircleProps omits children, size and value from the foundation Root props and defines its own size and value. With value={null} an arc travels around the circle while completion is unknown.

Read the installed source.

Usage notes

The application owns timing, cancellation and failures. Keep the accessible label stable as the value changes; use a separate message for a failure or completion that needs an announcement.