Feedback
Progress
A labelled bar showing how far a task has got, or that it is under way when the end is unknown.
- Built on
<progress>:indeterminate::-webkit-progress-value::-moz-progress-bar- Client JavaScript
- None
- Works without React
- Yes, as HTML and CSS
- Source, gzipped
- 1.5 kB
- Dependencies
- React
64%
Try its options
Loading its options.
Code
import { Progress } from "@/components/equip/progress";
export default function Demo() {
return (
<div style={{ display: "grid", gap: 16, width: 208 }}>
<Progress label="Uploading brand-kit.zip" value={64} valueText="64%" />
<Progress label="Restoring backup" />
</div>
);
}Your versionchange its options in Look, This part
HTMLworks in any framework, with the part's CSS
<div style="display:grid;gap:16px;width:208px">
<div class="eq-progress"><label><span class="eq-progress-label">Uploading brand-kit.zip</span><progress aria-valuetext="64%" class="eq-progress-bar" max="100" value="64"></progress></label><span class="eq-progress-value">64%</span></div>
<div class="eq-progress">
<label><span class="eq-progress-label">Restoring backup</span><progress class="eq-progress-bar" max="100"></progress></label>
</div>
</div>Examples
Counts instead of percent
A string valueText is also set as aria-valuetext, so screen readers hear "12 of 40 files" rather than 30 percent.
12 of 40 files
7.4 of 10 GB
Code
import { Progress } from "@/components/equip/progress";
export default function Example() {
return (
<div style={{ display: "grid", gap: 16, maxWidth: 320 }}>
<Progress label="Syncing photos" value={12} max={40} valueText="12 of 40 files" />
<Progress label="Storage used" value={7.4} max={10} valueText="7.4 of 10 GB" />
</div>
);
}HTMLworks in any framework, with the part's CSS
<div style="display:grid;gap:16px;max-width:320px">
<div class="eq-progress"><label><span class="eq-progress-label">Syncing photos</span><progress aria-valuetext="12 of 40 files" class="eq-progress-bar" max="40" value="12"></progress></label><span class="eq-progress-value">12 of 40 files</span></div>
<div class="eq-progress"><label><span class="eq-progress-label">Storage used</span><progress aria-valuetext="7.4 of 10 GB" class="eq-progress-bar" max="10" value="7.4"></progress></label><span class="eq-progress-value">7.4 of 10 GB</span></div>
</div>Empty, partway, done and indeterminate
Leave out value while the size of the job is unknown; value={0} is still a determinate bar.
0%
38%
Done
Code
import { Progress } from "@/components/equip/progress";
export default function Example() {
return (
<div style={{ display: "grid", gap: 16, maxWidth: 320 }}>
<Progress label="Queued: migrate users table" value={0} valueText="0%" />
<Progress label="Building larder-web" value={38} valueText="38%" />
<Progress label="Tests passed" value={100} valueText="Done" />
<Progress label="Waiting for a runner" />
</div>
);
}HTMLworks in any framework, with the part's CSS
<div style="display:grid;gap:16px;max-width:320px">
<div class="eq-progress"><label><span class="eq-progress-label">Queued: migrate users table</span><progress aria-valuetext="0%" class="eq-progress-bar" max="100" value="0"></progress></label><span class="eq-progress-value">0%</span></div>
<div class="eq-progress"><label><span class="eq-progress-label">Building larder-web</span><progress aria-valuetext="38%" class="eq-progress-bar" max="100" value="38"></progress></label><span class="eq-progress-value">38%</span></div>
<div class="eq-progress"><label><span class="eq-progress-label">Tests passed</span><progress aria-valuetext="Done" class="eq-progress-bar" max="100" value="100"></progress></label><span class="eq-progress-value">Done</span></div>
<div class="eq-progress">
<label><span class="eq-progress-label">Waiting for a runner</span><progress class="eq-progress-bar" max="100"></progress></label>
</div>
</div>Props
| Prop | Type | Default | What it does |
|---|---|---|---|
label | ReactNode | none | What is in progress. Wraps the bar in a label, so it becomes the accessible name. |
value | number | none | How far along, from 0 to max. Leave out for an indeterminate bar. |
max | number | 100 | The value at which the task is complete. |
valueText | ReactNode | none | Shown opposite the label. A string is also used as aria-valuetext on a determinate bar. |
className | string | none | Added to the root wrapper, so it sizes and places the whole part. |
...rest | ComponentProps<"progress"> | none | Every other native progress attribute, including id, ref, style and aria-describedby, goes on the bar itself. |
Anatomy
| root | .eq-progress | A two-row grid: label and value text, then the bar. |
|---|---|---|
| label | .eq-progress-label | |
| bar | .eq-progress-bar | The native progress element. |
| value | .eq-progress-value | Outside the label, so it is not read as part of the name. |
Notes
- Renders on the server with no client JavaScript; update value from wherever the work is tracked.
- The indeterminate bar animates on its own, as a progress indicator may; under reduced motion it slides at a slow, steady pace.
- Progress is not a live region. If finishing matters, say so in an Alert or other status message as well.
Source
Two files, and they are the whole part. Open either to read it, copy it straight from its bar, or add the part to your loadout and install it with everything else you picked.
components/equip/progress.tsx30 lines
import type { ComponentProps, ReactNode } from "react";
import "./progress.css";
export type ProgressProps = Omit<ComponentProps<"progress">, "value"> & {
/** What is in progress. Wraps the bar in its label, so it is the bar's accessible name. */
label: ReactNode;
/** Leave out for an indeterminate bar that only says work is under way. */
value?: number;
/** Shown opposite the label, such as "64%" or "12 of 40 files". A string is also used as aria-valuetext. */
valueText?: ReactNode;
};
export function Progress({ label, value, valueText, max = 100, className, ...rest }: ProgressProps) {
return (
<div className={["eq-progress", className].filter(Boolean).join(" ")}>
{/* display: contents, so the label's text and bar sit in the root's grid. */}
<label>
<span className="eq-progress-label">{label}</span>
<progress
aria-valuetext={typeof valueText === "string" && value !== undefined ? valueText : undefined}
{...rest}
className="eq-progress-bar"
max={max}
value={value}
/>
</label>
{valueText != null && <span className="eq-progress-value">{valueText}</span>}
</div>
);
}components/equip/progress.css94 lines
@layer equip {
.eq-progress {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: baseline;
gap: var(--eq-space-1) var(--eq-space-3);
font: var(--eq-weight) var(--eq-text-s) / 1.4 var(--eq-font);
}
/* The label element only ties the text to the bar; its children join the grid above. */
.eq-progress > label {
display: contents;
}
.eq-progress-label {
grid-area: 1 / 1;
min-width: 0;
color: var(--eq-ink);
font-weight: var(--eq-weight-strong);
}
.eq-progress-value {
grid-area: 1 / 2;
color: var(--eq-ink-2);
font-variant-numeric: tabular-nums;
}
/* The element's own background is the track in every engine. */
.eq-progress-bar {
grid-area: 2 / 1 / 3 / 3;
display: block;
width: 100%;
height: 6px;
margin: 0;
border: 0;
border-radius: var(--eq-radius-full);
overflow: hidden;
appearance: none;
background: var(--eq-tint-strong);
color: var(--eq-solid);
}
/* WebKit and Blink: the bar pseudo covers the element, so it stays clear. */
.eq-progress-bar::-webkit-progress-bar {
background: transparent;
}
.eq-progress-bar::-webkit-progress-value {
border-radius: var(--eq-radius-full);
background: var(--eq-solid);
/* A measurement: it travels, but never overshoots the value it reports. */
transition: width var(--eq-dur-move) var(--eq-ease);
}
/* Firefox. Kept in its own rule: an unknown pseudo-element would void a shared one. */
.eq-progress-bar::-moz-progress-bar {
border-radius: var(--eq-radius-full);
background: var(--eq-solid);
}
/* Indeterminate: both engines' fills are cleared and a segment is drawn on the track. */
.eq-progress-bar:indeterminate {
background:
linear-gradient(var(--eq-solid), var(--eq-solid)) no-repeat,
var(--eq-tint-strong);
background-size: 40% 100%;
animation: eq-progress-slide 1.4s cubic-bezier(0.65, 0, 0.35, 1) infinite;
}
.eq-progress-bar:indeterminate::-webkit-progress-value {
background: transparent;
}
.eq-progress-bar:indeterminate::-moz-progress-bar {
background: transparent;
}
/* With a 40% segment, -70% and 170% put it just past each end. */
@keyframes eq-progress-slide {
from {
background-position: -70% 0;
}
to {
background-position: 170% 0;
}
}
@media (prefers-reduced-motion: reduce) {
.eq-progress-bar:indeterminate {
animation-duration: 4s;
animation-timing-function: linear;
}
}
}