Browse docs
Browse docs
A display-only determinate progress indicator — you know the current position along a known total (upload bytes, wizard step, quiz completion, download percentage). Ships two visual flavors on the same primitive: horizontal linear bar and circular SVG arc.
import { Progress } from '@dashforge/tw';
<Progress value={40} fullWidth showLabel />Two primitives that look superficially similar but mean different things — pick by whether you know how far along the operation is.
Use <Progress> when... | Use <Spinner> when... |
|---|---|
| You know the current position along a known total | You don't know when the operation will finish |
The value is known and updates over time | No numeric state, just "please wait" |
| Screen readers should announce "42 percent complete" | Screen readers should announce "loading" / "busy" |
| Upload / download / wizard / quiz / scoring | Data fetch / API submit / initial page load |
Progress carries role="progressbar" + aria-valuenow/min/max. Spinner is role="status" + aria-busy="true". Two different a11y contracts, two different UX contracts.
If your operation starts unknown-ETA and later becomes known-ETA (e.g. resumable upload that first probes the server), start with <Spinner> and swap to <Progress> when the ETA lands.
import { Progress } from '@dashforge/tw';
// Simple percentage with the default formatter (rounded %).
<Progress value={40} fullWidth showLabel />
// Wizard step counter with a custom label.
<Progress
value={3}
max={5}
fullWidth
showLabel
formatLabel={(v, max) => `Step ${v} of ${max}`}
thickness="thick"
/>
// Circular flavor for KPI / dashboard tiles.
<Progress variant="circular" value={78} size="lg" showLabel />Default flavor. showLabel opts into the % label next to the bar. thickness controls the track height on top of size.
import { Progress, Stack, Typography } from '@dashforge/tw';
<Stack gap={4}>
<Progress value={40} fullWidth />
<Progress value={72} fullWidth showLabel />
<Progress
value={3}
max={5}
fullWidth
showLabel
formatLabel={(v, max) => `Step ${v} of ${max}`}
thickness="thick"
/>
{/* 6 colour intents */}
<Progress value={60} fullWidth color="primary" />
<Progress value={60} fullWidth color="secondary" />
<Progress value={60} fullWidth color="success" />
<Progress value={60} fullWidth color="warning" />
<Progress value={60} fullWidth color="danger" />
<Progress value={60} fullWidth color="neutral" />
</Stack>SVG arc, drawn starting at 12 o'clock and filling clockwise for the current ratio. The value label renders in the center of the arc when showLabel is set.
import { Progress, Stack } from '@dashforge/tw';
<Stack direction="row" gap={4}>
<Progress variant="circular" value={60} size="sm" showLabel />
<Progress variant="circular" value={60} size="md" showLabel />
<Progress variant="circular" value={60} size="lg" showLabel />
</Stack>
{/* Custom label + colour */}
<Progress
variant="circular"
value={82}
size="lg"
thickness="thick"
showLabel
formatLabel={(v) => `${v}pt`}
color="success"
/>Real-world case: value drives the bar during an active upload, and visibleWhen gates the entire component so it disappears when idle. The closure reads local state (state !== 'idle') with no DashForm in scope — that's supported natively, the predicate is just a JS closure.
import { useEffect, useState } from 'react';
import { Button, Progress, Stack } from '@dashforge/tw';
function UploadDemo() {
const totalBytes = 12 * 1024 * 1024; // 12 MB
const [uploaded, setUploaded] = useState(0);
const [state, setState] = useState<'idle' | 'uploading' | 'done'>('idle');
useEffect(() => {
if (state !== 'uploading') return;
// ... animate uploaded from 0 to totalBytes over ~4s
}, [state]);
return (
<Stack gap={3}>
<Button onClick={() => setState('uploading')}>Start upload</Button>
<Progress
value={uploaded}
max={totalBytes}
fullWidth
showLabel
formatLabel={(v, max) =>
`${(v / 1_048_576).toFixed(1)} MB / ${(max / 1_048_576).toFixed(1)} MB`
}
color={state === 'done' ? 'success' : 'primary'}
// Hidden while idle — closure reads local state without a DashForm.
visibleWhen={() => state !== 'idle'}
/>
</Stack>
);
}Configure <Progress> defaults application-wide via Option C.
import { patchTheme } from '@dashforge/tw-theme';
patchTheme({
components: {
Progress: {
defaults: {
variant: 'linear',
color: 'primary',
size: 'md',
fullWidth: true,
},
},
},
});Configurable axes (ProgressVariantProps):
| Axis | Type | Notes |
|---|---|---|
variant | 'linear' | 'circular' | Default 'linear'. |
color | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'neutral' | neutral auto-inverts via the preset CSS-var swap. |
size | 'sm' | 'md' | 'lg' | Typography + baseline geometry. |
thickness | 'thin' | 'md' | 'thick' | Track height (linear) / arc stroke-width (circular). Independent axis from size. |
fullWidth | boolean | Linear only — stretches to fill the parent. |
Precedence chain (lowest → highest):
defaultVariants (variant='linear', color='primary', size='md', fullWidth=false).theme.components.Progress.defaults.sx — appended to the root class chain via tailwind-merge.Same chain applies to slotProps (theme slotProps → instance slotProps).
| Prop | Type | Default | Description |
|---|---|---|---|
value | number | — | Current value. Clamped to [min, max] internally. Progress is a determinate indicator — indeterminate loading (unknown ETA) is the job of <Spinner>. |
access | AccessRequirement | — | RBAC gate — see useAccessState. |
aria-label | string | — | Explicit aria-label on the progressbar. When omitted, falls back to label (if a string) or to a generic "progress" label. |
color | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'neutral' | 'primary' | Colour intent for the filled portion. neutral auto-inverts via the preset CSS-var swap. |
formatLabel | (value: number, max: number) => ReactNode | — | Formatter for the value label. Receives the current value AND the max so consumers can render value/max compositions or free-form copy like Step 3 of 5 without an extra closure over max.Default returns the ratio rounded to a whole percent — e.g. value=42, max=100 renders 42%. |
fullWidth | boolean | — | Stretches the root to w-full (linear variant only). |
label | ReactNode | — | Optional field-style label above the control. |
max | number | 100 | Upper bound. |
min | number | 0 | Lower bound. |
name | string | — | Optional field name. Progress is display-only and does NOT register on the form bridge — this is used only for logging / a11y grouping. |
showLabel | boolean | false | When true, renders the value as a label — beside the bar (linear) or in the centre of the arc (circular). Default formatter is ${round((value / max) * 100)}%; override via formatLabel. |
size | 'md' | 'sm' | 'lg' | 'md' | Density knob. Controls typography of the value label and the base geometry for both linear (track height baseline) and circular (SVG viewport diameter). |
slotProps | ProgressSlotProps | — | Per-slot overrides. |
sx | string | — | Utility-class shortcut for slotProps.root.className — Dashforge idiom consistent with the rest of the catalog. |
testId | string | — | data-testid on the root wrapper. |
thickness | ProgressThickness | 'md' | Track height (linear) / arc stroke-width (circular). Independent axis from size so DSes can compose a "thin + large" or "thick + small" combo. |
variant | 'linear' | 'circular' | 'linear' | linear (bar) or circular (arc). Configurable via theme.components.Progress.defaults.variant. |
visibleWhen | (engine: Engine) => boolean | — | Reactive visibility predicate. The predicate closure can freely mix engine state (via the engine argument) and local state / context / refs captured in the outer scope — React re-render cascade guarantees fresh values on every evaluation. |
<Progress> renders through named slots. Each accepts a { className?: string } override via slotProps.
| Slot | Purpose |
|---|---|
root | Outer wrapper (label + control). sx is a shortcut for slotProps.root.className. |
label | The field-style label above the control (rendered only when label prop is set). |
control | The linear track wrapper OR the SVG viewport (circular). |
track | The grey background — linear bar OR circular ring. |
bar | The filled portion of the linear variant (width driven by value). |
arc | The filled arc of the circular variant (stroke-dashoffset driven by value). |
valueLabel | The % text (or the formatLabel result). Position differs by variant. |
value — if you don't have one, reach for <Spinner>. This isn't API bloat: <Spinner> and <Progress> map to role="status" (busy) and role="progressbar" (making-progress) respectively, and screen readers announce them differently. They're two different UX contracts.value > max or value < min doesn't throw — the component clamps to the bounds. The aria-valuenow reflects the clamped value, not the raw prop.visibleWhen is a closure (catalog-uniform pattern): the predicate can freely mix engine state (via the argument) and local state / context / refs captured in the outer scope. React re-render cascade guarantees fresh values on every evaluation. No useMemo needed.access is orthogonal to bridge integration: Progress is display-only (no form registration), but access still works via the RBAC context — useful for gating a metrics Progress bar to admin roles.transform: rotate(-90deg) the SVG so the fill starts at the top and grows clockwise, which is the near-universal convention.stroke-dashoffset both use CSS transitions gated on prefers-reduced-motion — users who opt out see instantaneous updates without animation.bridge.setValue, does NOT appear in the form data on submit. If you need a numeric input the user can drag, use <Slider>.<Slider>: if the value should be user-editable (drag / keyboard), it's a Slider — Progress is display-only.<Spinner>: if the ETA is unknown, it's a Spinner — Progress requires a value.