Progress

Linear bar or SVG ring that visualises how far along an operation is. Determinate when you know the value, indeterminate when you don't — same prop surface for both. Ships with four colour variants, three sizes, an animated striped overlay for active states, a circular shape with an inner label slot, and automatic prefers-reduced-motion handling.

Uploading…33%

Usage

import { Progress } from '@bwo-ui/react';

{/* Determinate */}
<Progress value={66} />

{/* Indeterminate (no value) */}
<Progress />

{/* Circular with centred label */}
<Progress shape="circular" value={45}>45%</Progress>

Variants

Four colour variants drive the fill. primary (black) is the neutral default; the semantic colours read like the rest of the kit — green for success or complete, yellow for caution / in-flight, red for blocking states (e.g. an upload that's about to fail a size cap).

primary
green
yellow
red
<Progress value={62} />               {/* primary */}
<Progress value={62} variant="green" />
<Progress value={62} variant="yellow" />
<Progress value={62} variant="red" />

Sizes

sm (4 px), md (8 px, default), and lg (12 px) for linear bars. Smaller bars read as ambient meta progress (in a list row or a card footer); larger bars work for centred hero states (upload page, install wizard). The same size scale controls circular diameter: 40 / 64 / 96 px.

sm
md
lg
<Progress value={48} size="sm" />
<Progress value={48}            />  {/* md */}
<Progress value={48} size="lg" />

Striped

Pass striped to overlay an animated diagonal pattern on the fill — the canonical "something is actively happening" treatment. Combine with green for "processing successfully", yellow for "pending review", red for "recovering".

<Progress value={62} striped size="lg" />
<Progress value={45} striped variant="green" size="lg" />
<Progress value={78} striped variant="yellow" size="lg" />
<Progress value={28} striped variant="red" size="lg" />

Indeterminate

Omit value and the bar enters indeterminate mode — a 40 %-wide indicator slides left to right continuously. Use it when you don't know how long an operation will take. The circular shape uses a fixed 70 % arc that spins instead, which reads as more "searching" than "loading."

<Progress />
<Progress variant="green" size="lg" />
<Progress shape="circular" />

Circular

Pass shape="circular" to render an SVG ring instead of a linear bar. Diameter follows the same size prop (40 / 64 / 96 px) and stroke width scales with it. Anything you pass as children renders centred over the ring — most commonly the percentage, but you can drop in any node (icon, badge, microcopy).

45%
45%
45%
45%
45%
45%
<Progress shape="circular" size="sm" value={45}>45%</Progress>
<Progress shape="circular"          value={45}>45%</Progress>
<Progress shape="circular" size="lg" value={45}>45%</Progress>

<Progress shape="circular" size="lg" variant="green"  value={75}>75%</Progress>
<Progress shape="circular" size="lg" variant="yellow" value={50}>50%</Progress>
<Progress shape="circular" size="lg" variant="red"    value={20}>20%</Progress>

Recipes

File upload

Multiple files in flight, each with its own bar. While a file is uploading, use a striped primary bar; when it finishes, swap to a flat green bar so the eye can spot completed work at a glance. Wrap each row in aria-busy while the upload is active.

design-spec.pdf1.4 MB · 0%
hero.mp424.8 MB · 0%
wordmark.svg12 KB · done
{files.map((file) => {
  const done = file.progress >= 100;
  return (
    <div key={file.name} aria-busy={!done}>
      <div style={{ display: 'flex', justifyContent: 'space-between' }}>
        <span>{file.name}</span>
        <span>{file.size} · {done ? 'done' : `${file.progress}%`}</span>
      </div>
      <Progress
        value={file.progress}
        variant={done ? 'green' : 'primary'}
        striped={!done}
        size="sm"
      />
    </div>
  );
})}

Wizard / multi-step flow

Show users how far they are through a multi-step flow. Compute the percentage from (currentStep + 1) / totalSteps and pair the bar with a short label ("Step 3 of 5").

Step 3 of 5Workspace
const STEPS = ['Account', 'Profile', 'Workspace', 'Invite team', 'Review'];

<div style={{ display: 'flex', justifyContent: 'space-between' }}>
  <span>Step {step + 1} of {STEPS.length}</span>
  <span>{STEPS[step]}</span>
</div>
<Progress value={((step + 1) / STEPS.length) * 100} size="sm" />

Profile completion

Encouraging users to finish setup is a classic Progress use case. Use yellow while there's still work to do; swap to green at 100 %.

Profile completion75 %

Add a cover photo and verify your email to reach 100 %.

<Card>
  <CardHeader>
    <CardTitle>
      <span style={{ display: 'inline-flex', alignItems: 'center', gap: 10 }}>
        Profile completion
        <Badge variant="yellow" size="sm">75 %</Badge>
      </span>
    </CardTitle>
  </CardHeader>
  <Progress value={75} variant="yellow" />
  <CardFooter>
    <Button size="sm" variant="primary">Finish setup</Button>
  </CardFooter>
</Card>

Determinate ↔ indeterminate toggle

A request that becomes determinate once it sends a first byte. Pass value={undefined} while waiting, then start updating with real numbers once they're available — the component handles both states from the same prop.

42%
const [known, setKnown] = useState(false);
const [pct, setPct] = useState(0);

return (
  <Progress
    value={known ? pct : undefined}
    striped
  />
);

Accessibility

Props

PropTypeDefaultDescription
valuenumberCurrent value (0 – `max`). Omit for indeterminate state — a sliding bar (linear) or spinning arc (circular).
maxnumber100Maximum value for the scale.
variant'primary' | 'green' | 'yellow' | 'red''primary'Fill colour. `primary` is black; the rest follow the semantic palette.
size'sm' | 'md' | 'lg''md'Linear: 4 / 8 / 12 px height. Circular: 40 / 64 / 96 px diameter (with proportional stroke).
shape'linear' | 'circular''linear'`linear` for horizontal bars, `circular` for an SVG ring with a centred children slot.
stripedbooleanAnimated diagonal-stripe overlay. Linear-only. Reads as "actively in progress".
radius'none' | 'sm' | 'md' | 'lg' | 'pill'Corner radius override. Linear-only. Omit to inherit the default pill radius.
childrenReactNodeCircular shape only — content rendered centred over the ring. Typically the percentage label, but can be any node.
…restHTMLAttributes<HTMLDivElement>Native div attributes are forwarded — `style`, `aria-label`, `onClick`, etc.