Multi-step loader
Free · MITCard or overlay that ticks through steps like "Reserving slot…" with spinners, drawn checkmarks and a filling rail, ending on an animated success state.
Installation
$ npx shadcn@latest add https://goodui.dev/r/multi-step-loader.jsonThe file lands in components/goodui/multi-step-loader.tsx and is yours to edit. With the @goodui registry added, npx shadcn@latest add @goodui/multi-step-loader works too.
Usage
import { MultiStepLoader } from "@/components/goodui/multi-step-loader"
import { Button } from "@/components/ui/button"
const steps = [
{ label: "Checking availability…", description: "Thu, Nov 6 · 2:30 PM with Priya" },
{ label: "Reserving your slot…", description: "Held for you for 10 minutes" },
{ label: "Charging the $40 deposit…", description: "Visa ending 4242" },
{ label: "Sending confirmation…", description: "Email and SMS to Maya Chen" },
]
export function BookingProgress({ step, onViewBooking }: { step: number; onViewBooking: () => void }) {
return (
<MultiStepLoader
steps={steps}
step={step}
title="Booking Balayage + gloss"
successTitle="You're booked"
successDescription="Thu, Nov 6 · 2:30 PM · Glow Studio"
successAction={
<Button size="sm" onClick={onViewBooking}>
View booking
</Button>
}
/>
)
}When to use
- A request that runs several stages the user should see: check availability, reserve, charge deposit, send confirmation.
- Imports and migrations ("Reading clients.csv", "Merging 18 duplicates") shown as a blocking overlay until done.
- Turning a long wait into visible progress, ending on a clear success state with a next action.
- A single short request: use spinners in the button, or a toast-stack promise toast.
- Steps the user fills in and navigates between: use stepper.
- Progress measured in percent or bytes: use progress.
| If you need | Use |
|---|---|
| Inline in a page or panel | variant="card" (default) |
| Block the page until done | variant="overlay" with open and onOpenChange |
| Driven by real work | step={index} (controlled); steps.length means done |
| Demo or optimistic timing | running with stepDuration={ms} or an array per step |
| Next action at the end | successAction={<Button …/>} |
Examples
Blocking import overlay
<MultiStepLoader
variant="overlay"
open={open}
onOpenChange={setOpen}
steps={["Reading clients.csv", "Matching 1,240 clients", "Merging 18 duplicates", "Importing visit history"]}
stepDuration={850}
title="Importing from Square"
successTitle="1,222 clients imported"
successAction={<Button size="sm" onClick={() => setOpen(false)}>Done</Button>}
/>Timed steps with a pause
<MultiStepLoader steps={steps} running={!paused} stepDuration={[1100, 900, 1300, 1000]} onComplete={() => track("booking_done")} />API reference
| Prop | Type | Default |
|---|---|---|
| steps* Steps in order: a string, or { label, description } where the description shows under the active step. | MultiStep[] | — |
| step Controlled active step (0-based). steps.length means everything is done. | number | — |
| running Auto-advance while true (uncontrolled only). | boolean | — |
| stepDuration Ms per step when auto-advancing. A number per step is allowed. | number | number[] | — |
| onStepChange Called with the new step index when auto-advancing (uncontrolled mode only). | (step: number) => void | — |
| onComplete Called once every step is done. | () => void | — |
| title Heading while the steps run. | React.ReactNode | — |
| successTitle Heading on the final success state, next to an animated check. | React.ReactNode | — |
| successDescription Line under successTitle. | React.ReactNode | — |
| successAction Rendered under the success message, e.g. a "View booking" button. | React.ReactNode | — |
| variant "overlay" covers the page with a blurred backdrop and a centered card. | "card" | "overlay" | "card" |
| open Overlay only. | boolean | true |
| onOpenChange Overlay only. Esc and the backdrop close it once all steps are done. | (open: boolean) => void | — |
| labels Text for built-in controls and screen readers; pass translations here. | Partial<MultiStepLoaderLabels> | — |
Also accepts every <div> attribute.
Accessibility
| Escape | Overlay only: closes it through onOpenChange(false), but only after every step is done. |
| Tab / Shift+Tab | Overlay only: cycles through focusable elements inside the dialog (e.g. successAction); focus cannot leave it. |
- An sr-only role="status" polite region announces the current step label (when it is a string) and then successTitle, or "Done" when successTitle is not a string.
- The overlay is role="dialog" with aria-modal, labelled by the title; it takes focus on open, keeps Tab and Shift+Tab inside, and gives focus back to the previously focused element on close.
- The "Step x of n" line and the fallback announcements go through labels (stepOf, done) for translation.
Motion
- The active step shows a spinner turning every 0.8 s. A finished step pops into a green circle (spring 520 / 24 from 0.4 scale) and its check draws in 280 ms.
- The line to the next step fills top-down in 350 ms after a 100 ms delay; the header rail fills on a spring (stiffness 300, damping 30).
- The active step's description opens to its height in 220 ms. At the end the header slides to a success-check and the successAction row opens after 200 ms.
- Overlay: the backdrop fades in 200 ms and the card springs from 0.95 scale (stiffness 420, damping 32).
- Reduced motion: icons, lines, descriptions and the overlay change with fades or instantly; the spinner keeps turning, slower (2.4 s).
Notes for AI
Coding agents read these rules through MCP and llms.txt, or paste this page with Copy page.
- Pick one mode: controlled (step, you advance it as work finishes) or timed (running + stepDuration). onStepChange only fires in timed mode.
- step is 0-based and step === steps.length means done; that is when onComplete fires and the success state shows.
- The overlay cannot be dismissed while running; Esc and backdrop clicks only close it after the last step. Give successAction a close button.
- To restart a timed run, change its key; there is no reset method.
- Keep step labels short and present-tense with an ellipsis ("Reserving your slot…"); put details in description.
- labels={{ stepOf: (s, n) => `Schritt ${s} von ${n}`, done: "Fertig" }} translates the built-in text; title and successTitle are props.
FAQ
How do I tie it to a real API call?
Keep the step index in state, pass it as step, and increment it as each stage of your request resolves. Set it to steps.length when the last one finishes.
Can a step fail?
There is no error state. On failure, close the overlay or replace the card and show the error with an alert or a toast.
Related
Check or X in a circle that draws its stroke, fills, then pops with a ray-and-ring burst, for payment and booking result screens.
Linear progress in four variants: spring-filled determinate, indeterminate double sweep, moving stripes and segmented steps.
Horizontal or vertical stepper where the connecting line fills between steps, numbers morph into drawn checkmarks, a current-step ring glides to the active step, errors shake, and completed steps are clickable.
Six CSS-only loaders (ring, dots, bars, pulse, orbit, typing) sized in px or presets and painted with currentColor.