Multi-step loader

Free · MIT

Card or overlay that ticks through steps like "Reserving slot…" with spinners, drawn checkmarks and a filling rail, ending on an animated success state.

MarkdownOpen in v0
Open in v0

Installation

$ npx shadcn@latest add https://goodui.dev/r/multi-step-loader.json

The 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

Use it for
  • 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.
Reach for something else
  • 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 needUse
Inline in a page or panelvariant="card" (default)
Block the page until donevariant="overlay" with open and onOpenChange
Driven by real workstep={index} (controlled); steps.length means done
Demo or optimistic timingrunning with stepDuration={ms} or an array per step
Next action at the endsuccessAction={<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

PropTypeDefault
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.
booleantrue
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

EscapeOverlay only: closes it through onOpenChange(false), but only after every step is done.
Tab / Shift+TabOverlay 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