Progress
One native progress element with its painting replaced. value and max are not props of this component — they ride the element, which is also what draws the fill and announces the number. Leave value out and the bar is indeterminate, because the platform already has that state.
- Stable
- React 19
- Zero JavaScript
- One DOM node
Code
<Progress value={40} max={100} aria-label="Uploading photos" /><progress class="wayra-progress" value="40" max="100" aria-label="Uploading photos"></progress>Two props of its own — color and size — and everything else comes from
ComponentProps<"progress">. There is no variant, no shape and no width:
a bar is one shape, and it always fills the box you put it in.
Values
Section titled “Values”value and max are the element’s attributes and reach it untouched. The
browser works out the geometry, and it reports the same numbers to assistive
technology without a single aria-* attribute from us.
Code
<Progress value={0} max={100} aria-label="0 percent" /><Progress value={33} max={100} aria-label="33 percent" /><Progress value={66} max={100} aria-label="66 percent" /><Progress value={100} max={100} aria-label="100 percent" /><progress class="wayra-progress" value="0" max="100" aria-label="0 percent"></progress><progress class="wayra-progress" value="33" max="100" aria-label="33 percent"></progress><progress class="wayra-progress" value="66" max="100" aria-label="66 percent"></progress><progress class="wayra-progress" value="100" max="100" aria-label="100 percent"></progress>max is free too, so a bar that counts files rather than percentages needs no
arithmetic: value={7} max={12}.
Indeterminate
Section titled “Indeterminate”Omit value. That is the whole API — there is no loading flag, because
:indeterminate is a state the element already has and the stylesheet keys on
it directly.
Code
<Progress aria-label="Preparing export" /><progress class="wayra-progress" aria-label="Preparing export"></progress>A band of the fill colour crosses the track in one direction, from the inline start to the inline end, then starts again. Each pass begins with the band just off the start edge and ends with it just off the far edge, so the restart happens where nobody sees it. In a right-to-left page the band runs right to left.
One pass takes 1.8 seconds (0.56 Hz). A slow swing back and forth, around one cycle every five seconds (0.2 Hz), is the kind of motion that bothers people who are sensitive to it, so the band never turns around and never moves that slowly.
It reads as work in progress the way a pulsing full-length bar — which reads as a warning light — does not.
The band is the fill itself. An indeterminate bar has no value, so the fill
collapses to nothing, but it is still there and still styleable: hand it a
width and it comes back. That is why the band carries the same pill ends as a
determinate fill, and why it can travel on translate, which the compositor
handles without repainting a frame.
Colors
Section titled “Colors”Code
<Progress value={60} max={100} color="primary" aria-label="primary" /><Progress value={60} max={100} color="secondary" aria-label="secondary" /><Progress value={60} max={100} color="accent" aria-label="accent" /><Progress value={60} max={100} color="neutral" aria-label="neutral" /><Progress value={60} max={100} color="info" aria-label="info" /><Progress value={60} max={100} color="success" aria-label="success" /><Progress value={60} max={100} color="warning" aria-label="warning" /><Progress value={60} max={100} color="error" aria-label="error" /><progress class="wayra-progress" value="60" max="100" data-color="primary" aria-label="primary"></progress><progress class="wayra-progress" value="60" max="100" data-color="secondary" aria-label="secondary"></progress><progress class="wayra-progress" value="60" max="100" data-color="accent" aria-label="accent"></progress><progress class="wayra-progress" value="60" max="100" data-color="neutral" aria-label="neutral"></progress><progress class="wayra-progress" value="60" max="100" data-color="info" aria-label="info"></progress><progress class="wayra-progress" value="60" max="100" data-color="success" aria-label="success"></progress><progress class="wayra-progress" value="60" max="100" data-color="warning" aria-label="warning"></progress><progress class="wayra-progress" value="60" max="100" data-color="error" aria-label="error"></progress>The fill is the role at full strength and the track is base-300 in every one
of them. That pair is what carries the value, so WCAG 1.4.11 asks 3:1 of it.
Every role is fitted to 4.5:1 against base-100, and in the default theme
base-300 sits 1.27:1 from base-100 in the light half and 1.46:1 in the dark
one, so a role that keeps its 4.5:1 keeps at least 3:1 over the track — with
little to spare in the dark half.
The Storybook Colors story asserts the colour chain rather than assuming it: it compares
the resolved --wayra-progress-color on each bar against that role’s own
token, so a broken data-color chain fails the build instead of quietly
painting eight identical bars.
Code
<Progress value={60} max={100} size="xs" aria-label="xs" /><Progress value={60} max={100} size="sm" aria-label="sm" /><Progress value={60} max={100} size="md" aria-label="md" /><Progress value={60} max={100} size="lg" aria-label="lg" /><Progress value={60} max={100} size="xl" aria-label="xl" /><progress class="wayra-progress" value="60" max="100" data-size="xs" aria-label="xs"></progress><progress class="wayra-progress" value="60" max="100" data-size="sm" aria-label="sm"></progress><progress class="wayra-progress" value="60" max="100" data-size="md" aria-label="md"></progress><progress class="wayra-progress" value="60" max="100" data-size="lg" aria-label="lg"></progress><progress class="wayra-progress" value="60" max="100" data-size="xl" aria-label="xl"></progress>| Size | Thickness |
|---|---|
xs |
4px |
sm |
5px |
md |
6px |
lg |
8px |
xl |
10px |
One number moves, and it is the only one there is to move: a bar has no text to
scale with it. The ladder comes off --wayra-size-selector, the same unit the
badges and the selection controls use, so one token override rescales all of
them together.
Bar or ring?
Section titled “Bar or ring?”Two components, not one prop. The bar is a native <progress>, where value,
max and the indeterminate state all come free from the element; a ring has to
be a <div role="progressbar"> with every one of those written by hand,
because a <progress> cannot be turned into an arc across browsers.
Nothing but a colour rule is shared between them — the fill is the role, the
track is base-300, stated separately in each stylesheet — so
RadialProgress is its own component rather than
a shape prop that would leave half the props meaningless on either side.
API reference
Section titled “API reference”ProgressProps extends ComponentProps<"progress"> minus color.
| Prop | Type | Default | Description |
|---|---|---|---|
color |
WayraColor |
"primary" |
The fill. All eight roles, on a base-300 track. |
size |
"xs" | "sm" | "md" | "lg" | "xl" |
"md" |
Bar thickness, off --wayra-size-selector. |
unstyled |
boolean |
false |
Drops the class, keeps the data attributes. |
value, max, id, className and ref come free from the native element,
which is all any of this needs — there is no state to own and no handler to
attach.
Styling contract
Section titled “Styling contract”The class and the data attributes are public API. This markup is styled with no React in sight:
<progress class="wayra-progress" data-color="success" data-size="lg" value="40" max="100" aria-label="Uploading photos"></progress>Accessibility
Section titled “Accessibility”- A name is mandatory. Pass
aria-labeloraria-labelledby. A bar has no text of its own, so without one it announces a number attached to nothing — and the axe gate in Storybook fails any story that forgets. - Name the work, not the number. The element already reports the value; the name is there to say what is progressing.
- The value announces itself.
<progress>maps torole="progressbar"witharia-valuenowandaria-valuemaxderived from its own attributes. Adding them by hand only creates something that can fall out of sync. - It is not focusable and takes no place in the tab order. It reports; it never receives.
- Indeterminate is announced as such. With no
valuethe element exposes noaria-valuenow, which is what tells assistive technology the length is unknown. A visually hidden status message is worth adding when the wait is long. - Reduced motion stops the sweep, not the bar. Under
prefers-reduced-motionthe animation is dropped and the fill takes the whole track instead, so the state still reads — a parked band is indistinguishable from a determinate bar stopped at that number. The determinate fill stops animating its length there too, and jumps to the new value. - Contrast: the fill against the track has to clear 3:1 (WCAG 1.4.11).
Every role is fitted to 4.5:1 against
base-100, and thebase-300track sits 1.27:1 (light) and 1.46:1 (dark) from it, which leaves at least 3:1. - Windows high contrast re-points the pair at
HighlightoverCanvasand draws the track’s extent with aCanvasTextoutline, since the forced palette would otherwise flatten fill and track into the same colour.
Do and don’t
Section titled “Do and don’t”Do
Name what is progressing. The element announces the number on its own, so the name carries the subject.
Don't
A bar with no accessible name. It announces “40%” of nothing.
Do
Leave value out while the length of the work is unknown. The sweep says work is happening without claiming a number.
Don't
A made-up 90% that will sit there until the work finishes. A number nobody can trust is worse than no number.