ProgressBar
How far along something is.
Overview
How far along something is.
First example
<ProgressBar value={40}>Téléchargement</ProgressBar>
Anatomy
<ProgressBar value={40}>
<ProgressBar.Header>
<ProgressBar.Label />
<ProgressBar.Value />
</ProgressBar.Header>
<ProgressBar.Track>
<ProgressBar.Fill />
</ProgressBar.Track>
</ProgressBar>
ProgressBar— the root. It clamps the value once, resolves the recipe once (R5) and publishes both to its slots.ProgressBar.Header— the line above the rail, its two halves pushed apart. It exists because R4 puts the gap and the alignment on a root rather than on the things being spaced.ProgressBar.Label— what is happening.ProgressBar.Value— how far along, as text. Formats itself.ProgressBar.Track— the rail: the whole of the distance.ProgressBar.Fill— the part of it that is behind you.
The fill is a child of the rail, not a layer over it. It grows to a percentage of the
width and the rail clips it, so one radius rounds both — an absolutely positioned overlay
would need a corner of its own and would get it wrong at 100%.
Usage
R3 — the short form
<ProgressBar value={40}>Téléchargement</ProgressBar>
A text child becomes the label, and the bar comes with it: a progress bar with no bar is
a line of text. The Radio's rule, for the Radio's reason.
Written with no children at all it is the rail alone, which is the form a list row wants:
<ProgressBar value={40} size="sm" />
The range
<ProgressBar value={0.4} minValue={0} maxValue={1} />
<ProgressBar value={7} maxValue={12} />
0 to 100 by default. Every argument is a caller's number, so all three are validated
once at the root: a value outside the range is clamped, an inverted or empty range reads as
empty, and anything that is not finite reads as zero. A fill of NaN% is something React
Native accepts and draws as nothing at all, with no error anywhere.
The number beside it
<ProgressBar value={40} /> // 40 %
<ProgressBar value={1250} maxValue={2000}
formatOptions={{ style: 'currency', currency: 'EUR' }} /> // 1 250,00 €
<ProgressBar.Value>7 sur 12</ProgressBar.Value> // yours
formatOptions is Intl.NumberFormat's, and which number it formats follows the style:
a percentage is a share of the range, so it formats the fraction; anything else is about the
quantity, so it formats the value. Formatting the fraction as euros would report a 1 250 €
goal as 0,63 €.
A Hermes build compiled without ICU has no Intl at all; the fallback is the same number
with a plain percent sign rather than an exception.
The value is in tabular-nums, so a number ticking from 9 % to 10 % does not shift the
label beside it.
The motion
The fill sweeps to each new width over 240ms. A bar that jumped would render a download reporting every 5% as twenty still frames.
<ProgressBar.Fill animation={false} />
false snaps — for a value the caller is already animating itself.
Root props
ProgressBarProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| variant | ProgressBarVariant | undefined | No | — |
| size | ProgressBarSize | undefined | No | The rail's thickness and the header's type. A bar's width is its parent's business. |
| radius | RadiusKey | undefined | No | Overrides the corner, which is `full` on both the rail and the fill. |
| color | string | undefined | No | A raw tint (`'#7c3aed'`), never a token (R7). It lands on the fill. |
| value | number | undefined | No | How far along. Clamped into `[minValue, maxValue]`. |
| minValue | number | undefined | No | — |
| maxValue | number | undefined | No | — |
| formatOptions | NumberFormatOptions | undefined | No | How `ProgressBar.Value` reads the fraction. Anything `Intl.NumberFormat` takes — a currency, a unit, more decimals. |
| isDisabled | boolean | undefined | No | Dims the bar. There is nothing to press, so nothing else changes. |
| style | StyleProp<ViewStyle> | No | — |
| children | ReactNode | No | — |
Slots
ProgressBarFillProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| children | ReactNode | No | — |
| animation | boolean | undefined | No | `false` snaps to the new width — for a value the caller is already animating. |
ProgressBarTextSlotProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| children | ReactNode | No | — |
ProgressBarViewSlotProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| children | ReactNode | No | — |
Variants, sizes and colour
Sizes
size | Rail | Header type |
|---|---|---|
sm | 4 | 14/20 |
md | 6 | 16/24 |
lg | 8 | 18/28 |
size is the rail's thickness, never its width. A bar's length is its parent's, exactly
as a Button's is — which is why there is no fullWidth and why the root spans by default.
The rail's numbers are off the spacing grid on purpose, like the Slider's: how thin a line
can be and still read as a bar has nothing to do with the gaps between things.
radius is full on both the rail and the fill, and it moves both — a squared-off rail
holding a rounded fill is the thing the axis exists to prevent.
Variants and colour
variant | Rail | Fill |
|---|---|---|
primary | default | accent |
secondary | default | foreground |
success | default | success |
warning | default | warning |
danger | default | danger |
Five of the ten, and the five that are left out say what this is. tertiary and ghost are
gone because a fill with no fill is not a progress bar; the *-soft pairs are gone because
the rail already is the soft half of every one of these, and a soft fill on a soft track is
one bar you cannot read.
The rail is the same neutral under all five: it is the room left to go, and that is not success, warning or danger.
color is a raw value (R7) and lands on the fill, through the bgSelected role — the
Checkbox's pair, meaning the same thing on a line instead of in a box. Why a role and not
an axis is written up once in checkbox.md.
Accessibility
accessibilityRole="progressbar"on the root, overridable.accessibilityValuecarries the caller's range and value —{ min, max, now }— not the clamped fraction: "40 sur 100" is what the caller wrote, and rounding it to a percentage here would be answering a question nobody asked.- The label is a
Textinside the root, so it is read with the value rather than beside it.
Migration from legacy
Implementation notes
There is no `isIndeterminate`
An unknown duration is a Spinner. That is the split the legacy
Indicator was two components pretending to be one, and a bar that runs a loop across
itself is a spinner drawn as a line.
This one reports a quantity, and a quantity it does not have is not a state it should be able to be in.
Alignment with the reference implementation
Identical: the three sizes, the label-and-value header, the clamped range with
minValue / maxValue, formatOptions through Intl, and Track · Fill as slots.
Three deltas:
| Theirs | Ours | Why |
|---|---|---|
color="success" | variant="success" | Their color is our variant: an enum of intents is the design system's vocabulary, and color here is a raw tint (R7). |
isIndeterminate | Spinner | Two components pretending to be one. A bar with no quantity is a spinner drawn as a line. |
TrackBackground under Track | Track alone | Two nodes for one rail. The rail is the background; the second one existed to be themed separately, which style on the slot does. |
Extending it
useProgressBar() is exported (R10) and carries the resolved styles plus fraction,
value, formatOptions and isDisabled — enough to write a second line under the bar, an
estimate beside it, or a mark at the point it has to reach, without recomputing a fraction
the root already clamped. Outside a <ProgressBar> it throws by name.