caliper uidocs

Progress button

Free

A button that fills with determinate progress while its work runs, writes the percentage, and settles on a check.

Installs components/ui/progress-button.tsxDepends on @radix-ui/react-slot, lucide-reactMotion none in component source
pnpm dlx shadcn@latest add https://caliperui.site/r/progress-button.json

When to use #

Use it for

  • Exports, uploads and syncs started from a button.
  • Work that takes seconds and has a known size.

Skip it when

  • Unknown durations; use the button's loading state.

Installation #

terminal
pnpm dlx shadcn@latest add https://caliperui.site/r/progress-button.json

Adds its 3 files to components/ui/, each with its own stylesheet, and installs their packages. No account needed. The first part also adds Caliper UI’s tokens; import them once as in Installation.

Usage #

This is the code behind the preview above.

example.tsx
import { useEffect, useRef, useState } from "react"
import { ProgressButton } from "@/components/ui/progress-button"

export function ProgressButtonDemo() {
  const [progress, setProgress] = useState<number | null>(null);
  const [done, setDone] = useState(false);
  const timer = useRef<ReturnType<typeof setInterval>>(undefined);
  useEffect(() => () => clearInterval(timer.current), []);
  function start() {
    if (done) {
      setDone(false);
      setProgress(null);
      return;
    }
    let value = 0;
    setProgress(0);
    timer.current = setInterval(() => {
      value = Math.min(100, value + 7 + Math.random() * 12);
      setProgress(value);
      if (value >= 100) {
        clearInterval(timer.current);
        setDone(true);
      }
    }, 260);
  }
  return (
    <ProgressButton progress={progress} done={done} workingLabel="Exporting" doneLabel="Exported" onClick={start}>
      {done ? "Export again" : "Export 2,481 rows"}
    </ProgressButton>
  );
}

API reference #

ProgressButton

component

Also accepts the native props of its root element and a ref.

PropTypeDefaultDescription
size"sm" | "md" | "lg" | "icon"—
variant"default" | "secondary" | "ghost" | "destructive"—
asChildboolean—
feedbackKeystring—Animate a meaningful label/icon change, such as saving → saved.
progress *number | null—0 to 100 while working; null when idle.
donebooleanfalse
workingLabelstring"Working"Written before the percentage, e.g. "Exporting".
doneLabelstring"Done"
children *ReactNode—

Accessibility #

It is built on native elements and keeps aria-disabled, aria-busy and aria-hidden in step with state.

No custom key handling; Tab and the native activation keys do the work.

Motion #

It declares no animation in its component source. Any transition comes from its stylesheet, which uses the duration tokens that collapse to instant under prefers-reduced-motion.