Skip to main content
Clever Ops

Progress Button

A button that becomes its own progress state, idle, a hairline sweep tracing the border while working, a drawn check on success. Controlled state prop with honest aria-busy announcements.

Preview & code

Loading Progress Button preview

Installation

pnpm dlx shadcn@latest add @cleverui/button-progress

Requires the @cleverui registry in your components.json, a one-off, two-line setup.

Usage

tsx
import { ButtonProgress } from '@/components/ui/button-progress'
Exampletsx
'use client'

import { useCallback, useEffect, useRef, useState } from 'react'
import { ButtonProgress } from '@/components/ui/button-progress'

export default function ButtonProgressDemo() {
  const [deployState, setDeployState] = useState<'idle' | 'working' | 'success'>('idle')
  const [exportState, setExportState] = useState<'idle' | 'working' | 'success'>('idle')
  const [exportProgress, setExportProgress] = useState(0)
  const timers = useRef<ReturnType<typeof setTimeout>[]>([])

  const clearTimers = useCallback(() => {
    timers.current.forEach(clearTimeout)
    timers.current = []
  }, [])

  useEffect(() => () => clearTimers(), [clearTimers])

  const runDeploy = () => {
    if (deployState !== 'idle') return
    clearTimers()
    setDeployState('working')
    timers.current.push(
      setTimeout(() => setDeployState('success'), 2400),
      setTimeout(() => setDeployState('idle'), 4200)
    )
  }

  const runExport = () => {
    if (exportState !== 'idle') return
    clearTimers()
    setExportState('working')
    setExportProgress(0)
    const steps = [0.18, 0.42, 0.67, 0.88, 1]
    steps.forEach((p, i) => {
      timers.current.push(
        setTimeout(() => {
          setExportProgress(p)
          if (p === 1) {
            setExportState('success')
            timers.current.push(setTimeout(() => {
              setExportState('idle')
              setExportProgress(0)
            }, 1600))
          }
        }, 400 + i * 450)
      )
    })
  }

  return (
    <div className="mx-auto flex min-h-[380px] max-w-lg flex-col items-center justify-center gap-12 px-6 py-16">
      <div className="flex w-full flex-col items-center gap-3 text-center">
        <p className="font-mono text-[11px] uppercase tracking-[0.2em] text-muted-foreground">
          Harbor · production
        </p>
        <p className="max-w-sm text-sm leading-relaxed text-muted-foreground">
          Push build <span className="font-medium text-foreground">v2.14.3</span> to the
          edge fleet, 12 regions, zero-downtime rollout.
        </p>
        <ButtonProgress
          state={deployState}
          onClick={runDeploy}
          workingLabel="Deploying…"
          successLabel="Live"
        >
          Deploy to production
        </ButtonProgress>
      </div>

      <div className="flex w-full flex-col items-center gap-3 border-t border-border/60 pt-10 text-center">
        <p className="font-mono text-[11px] uppercase tracking-[0.2em] text-muted-foreground">
          Audit export · determinate
        </p>
        <p className="max-w-sm text-sm leading-relaxed text-muted-foreground">
          Compile the last 90 days of access logs into a signed CSV package.
        </p>
        <ButtonProgress
          state={exportState}
          progress={exportState === 'idle' ? undefined : exportProgress}
          onClick={runExport}
          workingLabel="Packaging…"
          successLabel="Downloaded"
        >
          Export access logs
        </ButtonProgress>
      </div>
    </div>
  )
}

API reference

ButtonProgress accepts the following props.

PropTypeDefaultDescription
state'idle' | 'working' | 'success''idle'fully controlled, the component never advances state on its own
progressnumber0–1; when provided the border sweep is determinate; omit for indeterminate orbit
children*React.ReactNodeidle label, e.g. 'Deploy to production'
workingLabelstring'Working…'label and aria-live announcement while state is working
successLabelstring'Done'label and announcement when state lands on success

…plus everything from Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'children'>: className, event handlers, aria attributes and the rest pass straight through.

Frequently asked questions

How do I install the Progress Button component?

Register the @cleverui registry in your components.json ("@cleverui": "https://cleverops.com.au/r/{name}.json"), then run `npx shadcn@latest add @cleverui/button-progress`. The shadcn CLI copies the source into your project, you own the code from that point. You can also copy the source directly from this page.

What dependencies does Progress Button require?

Progress Button uses motion on top of React and Tailwind CSS. You don't need to install it manually, the shadcn CLI resolves and installs npm dependencies automatically when you add the component.

Can I theme Progress Button to match my brand?

Yes. The component is styled entirely with Tailwind utilities over standard design tokens (primary, background, muted, border), so it inherits your existing shadcn/ui theme automatically. Change your CSS variables and the component follows, no source edits needed for palette, radius, or fonts.

Want this built for you?

CleverUI is built by the Clever Ops web team. If you'd rather ship a hand-coded, fast, custom website than assemble one, we build them for Australian businesses every week.

Explore web development services