Step loader
FreeWhat 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.
pnpm dlx shadcn@latest add https://caliperui.site/r/step-loader.json- Connect to the bank, in progress
- Fetch 12 months of transactions, waiting
- Match payments to invoices, waiting
- Apply your rules, waiting
- 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 #
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.
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
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.