Skip to content
WANCRAFT

A.123Buttons & Controls

Progress Button

A download button whose fill sweeps across as work progresses, with pause, cancel, retry and a rolling percentage.

NewReactMotionSVGTailwindadvanced

Overview

Progress Button turns a long-running action into one honest control. Press it and a solid fill sweeps left to right, easing towards each reported value on a spring, while the percentage rolls digit by digit. Press again to pause (the fill hatches over) or resume; a separate cancel button slides out beside it, and Escape cancels too. On success a check draws itself in, on failure the button shakes, turns red and retries when pressed. Hand it an async onStart(report, signal) and it runs the whole cycle — report() even stays pending while paused so your loop can honour it — or drive progress and status yourself. A visually hidden progressbar exposes aria-valuenow, the button is described by the current percentage, and a polite live region announces only the 25 / 50 / 75 / 100% milestones.

01Installation

Method 01 · Registry

CLI verified

Uses the shadcn CLI in a project initialised with shadcn init. Installs the file to components/ui/progress-button.tsx and adds its dependencies.

npx shadcn@latest add https://wancraft.my/r/progress-button.json

Method 02 · Copy and paste

  1. 1. Install the dependencies below.
  2. 2. Copy the source from the Code tab into components/ui/progress-button.tsx.
  3. 3. Import it and adjust props.
npm install motion
Dependencies
motion
Peer
react ≥ 18, tailwindcss ≥ 4
Added
13 Oct 2026
Updated
—

02Usage

The default configuration. Change properties above to generate a tailored example in the Usage tab.

example.tsx
import { ProgressButton, type ProgressReport } from "@/components/ui/progress-button";

export function ProgressButtonExample() {
  async function download(report: ProgressReport, signal: AbortSignal) {
    for (let step = 1; step <= 20 && !signal.aborted; step++) {
      await new Promise((resolve) => setTimeout(resolve, 150));
      // Stays pending while paused, rejects once cancelled.
      await report(step / 20);
    }
  }

  return (
    <ProgressButton onStart={download} />
  );
}

03Props

PropTypeDefaultDescription
onStart(report: ProgressReport, signal: AbortSignal) => Promise<unknown> | unknown—Uncontrolled mode: the task. Call report(0..1) as it advances; resolve → complete, reject → error. signal aborts on cancel / unmount.
progressnumber—Controlled progress, 0–1.
status"idle" | "running" | "paused" | "complete" | "error"—Controlled status. Presses then only request changes via onStatusChange; onStart is not called.
onStatusChange(status) => void—Every status change (uncontrolled) or requested change (controlled).
onPause / onResume / onCancel / onComplete() => void—Lifecycle callbacks, called in both modes (onComplete only when onStart resolves).
onError(error: unknown) => void—Receives the rejection reason when onStart fails.
labelstring"Download"Idle label and base of the accessible names (Pause / Resume / Retry / Cancel …).
runningLabel / pausedLabelstring"Downloading" / "Paused"Visible labels while running and paused.
completeLabel / errorLabelstring"Downloaded" / "Failed · Retry"Visible and announced labels for the result states.
pausablebooleantruePressing while running pauses; pressing again resumes.
cancellablebooleantrueShows a cancel button while running or paused; Escape on the button cancels too.
resetDelaynumber2400Time in ms before complete returns to idle (uncontrolled). 0 keeps it complete.
variant"fill" | "bar""fill"Solid fill sweeping across the button, or a hairline bar under the label.
spring{ stiffness: number; damping: number; mass: number }{ stiffness: 140, damping: 24, mass: 0.6 }Spring easing the fill towards each reported value.
accentstring"#FF7448"Fill, bar and focus-ring colour.
inkColor / errorColorstring"#0B0D0F" / "#F87171"Text colour over the fill, and the fill colour on error.
widthnumber | string"14rem"Width of the main button; it shrinks to fit narrow containers (labels truncate).
iconReactNode—Optional idle icon (replaces the download glyph).
...propsButtonHTMLAttributes—Forwarded to the main <button>; the ref points to it.

04Features

  • 01Fill sweeps left to right with a clip-path reveal, so the label inverts exactly at the edge
  • 02Percentage rolls digit by digit as progress is reported
  • 03Pause / resume on the main button, cancel on a side button or Escape
  • 04report() stays pending while paused and rejects on cancel, so tasks pause for real
  • 05Check draws in on completion; error shakes, turns red and retries on press
  • 06Controlled (progress + status) or uncontrolled (async onStart with AbortSignal)
motion · springmotion · tweeninput · clickinput · pressinput · keyboard

05Accessibility

  • Native buttons in a labelled group; the main button's name follows its action (Download, Pause Download, Resume Download, Retry Download).
  • A visually hidden role="progressbar" exposes aria-valuenow / aria-valuetext, and the button is aria-describedby the current percentage.
  • A polite live region announces only milestones (25, 50, 75, 100%), pauses, completion, failure and cancellation.
  • Focus moves back to the main button when the cancel button disappears; Escape cancels from the keyboard.
  • Focus-visible rings on both buttons; the complete state is aria-disabled rather than disabled, so focus is kept.
  • With prefers-reduced-motion the fill jumps to each value, digits swap without rolling and labels crossfade.

06Limitations

  • Pause only truly pauses the work if your onStart awaits report(); otherwise it pauses the visuals.
  • In controlled mode the button never resets itself — set status back to "idle" when you are ready.