caliper uidocs

Step loader

Free

What a long operation is doing: done steps tick off, the current one turns with its detail, the line fills, and a failure says where and why.

Installs components/ui/step-loader.tsxDepends on lucide-reactMotion none in component source
pnpm dlx shadcn@latest add https://caliperui.site/r/step-loader.json
  1. Connect to the bank, in progress
  2. Fetch 12 months of transactions, waiting
  3. Match payments to invoices, waiting
  4. Apply your rules, waiting
  5. Write the summary, waiting

Step 1 of 5: Connect to the bank.

When to use #

Use it for

  • Imports, migrations, provisioning and first syncs.
  • Any wait longer than a few seconds that has stages.

Skip it when

  • Quick actions; a button spinner is enough.

Installation #

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

Adds its 2 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, useState } from "react"
import { Button } from "@/components/ui/button"
import { StepLoader } from "@/components/ui/step-loader"

const importSteps = [
  { id: "connect", label: "Connect to the bank" },
  { id: "fetch", label: "Fetch 12 months of transactions", detail: "1,280 transactions" },
  { id: "match", label: "Match payments to invoices", detail: "Matched 214 of 1,280" },
  { id: "rules", label: "Apply your rules" },
  { id: "report", label: "Write the summary" },
];

export function StepLoaderDemo() {
  const [current, setCurrent] = useState(0);
  const [status, setStatus] = useState<"running" | "done" | "error">("running");
  useEffect(() => {
    if (status !== "running") return;
    const id = setTimeout(() => {
      if (current >= importSteps.length - 1) setStatus("done");
      else setCurrent((n) => n + 1);
    }, 1100);
    return () => clearTimeout(id);
  }, [current, status]);
  return (
    <div className="grid w-full max-w-sm gap-4">
      <StepLoader steps={importSteps} current={current} status={status} error="The bank refused the connection. Sign in to the bank again." />
      <div className="flex gap-2">
        <Button size="sm" variant="secondary" onClick={() => { setCurrent(0); setStatus("running"); }}>Run again</Button>
        <Button size="sm" variant="ghost" disabled={status !== "running"} onClick={() => setStatus("error")}>Make it fail</Button>
      </div>
    </div>
  );
}

API reference #

StepLoader

component
PropTypeDefaultDescription
steps *readonly LoaderStep[]—
current *number—Index of the step in progress; steps before it are done.
status"error" | "done" | "running""running"
errorstring—Why the current step failed.
classNamestring—

Accessibility #

It exposes status semantics, with aria-hidden kept in step with state. Changes that matter are announced through a live region.

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.