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.
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).
primarygreenyellowred<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.
smmdlg<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).
<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.
{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").
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.
const [known, setKnown] = useState(false);
const [pct, setPct] = useState(0);
return (
<Progress
value={known ? pct : undefined}
striped
/>
);Accessibility
- Progress renders with
role="progressbar"and the appropriatearia-valuemin/aria-valuemax/aria-valuenow(the last is omitted when indeterminate, per WAI-ARIA). - If the visible percentage is the only label, wrap the bar in an element with
aria-labeldescribing what's loading ("Uploading file", "Profile setup") — the number alone isn't enough context. - For batch operations (file lists, multi-step uploads), put
aria-busy="true"on the row that's in flight and remove it on completion. Screen readers will announce the change. - Don't rely on colour alone to convey state — pair
variant="red"with a text label that says what's wrong. - All animations (slide, stripe motion, circular spin) are disabled automatically when the OS reports
prefers-reduced-motion: reduce.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
value | number | — | Current value (0 – `max`). Omit for indeterminate state — a sliding bar (linear) or spinning arc (circular). |
max | number | 100 | Maximum 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. |
striped | boolean | — | Animated 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. |
children | ReactNode | — | Circular shape only — content rendered centred over the ring. Typically the percentage label, but can be any node. |
…rest | HTMLAttributes<HTMLDivElement> | — | Native div attributes are forwarded — `style`, `aria-label`, `onClick`, etc. |