Progress
A progress bar: one track with one span per part. A span's width is its share.
The markup is a track and a span for each part
The span's inline width is its part of the track. A bar that shows
a single value gets role="progressbar" and
aria-valuenow on the track.
<div class="progress" role="progressbar" aria-label="Uploading" aria-valuenow="66" aria-valuemin="0" aria-valuemax="100">
<span style="width: 66%"></span>
</div>
Height with data-size: sm 4px, default 6px, lg 12px, xl 32px
<div class="stack">
<div class="progress" data-size="sm" role="progressbar" aria-label="Small, 4 of 6 steps" aria-valuenow="66" aria-valuemin="0" aria-valuemax="100">
<span style="width: 66%"></span>
</div>
<div class="progress" role="progressbar" aria-label="Default, 4 of 6 steps" aria-valuenow="66" aria-valuemin="0" aria-valuemax="100">
<span style="width: 66%"></span>
</div>
<div class="progress" data-size="lg" role="progressbar" aria-label="Large, 4 of 6 steps" aria-valuenow="66" aria-valuemin="0" aria-valuemax="100">
<span style="width: 66%"></span>
</div>
<div class="progress" data-size="xl" role="progressbar" aria-label="Extra large, 4 of 6 steps" aria-valuenow="66" aria-valuemin="0" aria-valuemax="100">
<span style="width: 66%"></span>
</div>
</div>
data-variant on a span sets its colour
Without it, the span uses the main text colour (accent is only for links and primary buttons). The others are ok, warning, info and danger.
<div class="stack">
<div class="progress" role="progressbar" aria-label="Product import" aria-valuenow="45" aria-valuemin="0" aria-valuemax="100">
<span style="width: 45%"></span>
</div>
<div class="progress" role="progressbar" aria-label="Storage used" aria-valuenow="30" aria-valuemin="0" aria-valuemax="100">
<span style="width: 30%" data-variant="ok"></span>
</div>
<div class="progress" role="progressbar" aria-label="Storage used" aria-valuenow="78" aria-valuemin="0" aria-valuemax="100">
<span style="width: 78%" data-variant="warning"></span>
</div>
<div class="progress" role="progressbar" aria-label="Stock sync" aria-valuenow="55" aria-valuemin="0" aria-valuemax="100">
<span style="width: 55%" data-variant="info"></span>
</div>
<div class="progress" role="progressbar" aria-label="Storage used" aria-valuenow="96" aria-valuemin="0" aria-valuemax="100">
<span style="width: 96%" data-variant="danger"></span>
</div>
</div>
A label and the value above the track
Put a <header> right before the track, with the
label on the left and the value on the right. Link the label with
aria-labelledby and give a readable value with
aria-valuetext.
<div>
<header>
<span id="pg-storage-label">Product image storage</span>
<span>7.8 GB of 10 GB</span>
</header>
<div class="progress" role="progressbar" aria-labelledby="pg-storage-label" aria-valuenow="78" aria-valuemin="0" aria-valuemax="100" aria-valuetext="7.8 GB of 10 GB">
<span style="width: 78%" data-variant="warning"></span>
</div>
</div>
Several spans divide the track
Parts without a colour go from dark to light. A bar split into
parts uses role="img" and an
aria-label that names every part. Use at most 5
parts.
<div class="progress" data-size="lg" role="img" aria-label="Orders by fulfillment: 62% fulfilled, 23% processing, 15% unfulfilled">
<span style="width: 62%"></span>
<span style="width: 23%"></span>
<span style="width: 15%"></span>
</div>
data-variant="empty" leaves a gap
An empty span draws nothing, so the next part starts
mid-track. Good for ranges or values compared with a midpoint.
<div class="stack">
<div class="progress" role="img" aria-label="Risk score raised 25.3 times">
<span data-variant="empty" style="width: 50%"></span>
<span data-variant="danger" style="width: 35%"></span>
</div>
<div class="progress" role="img" aria-label="Risk score lowered to 0.4 times">
<span data-variant="empty" style="width: 40%"></span>
<span data-variant="ok" style="width: 10%"></span>
</div>
</div>
The extra large track with a list under it shows how a total divides
A <ul> right after the track gets markers that
match the part in the same position. The number goes in the last
span of each row.
Cancellations by reason
678 cancellations in the last 30 days
- Other60%
- Recurring payment declined23%
- Not satisfied with quality9%
- Too expensive5%
- Changed my mind3%
<div class="card">
<header>
<h2>Cancellations by reason</h2>
<p>678 cancellations in the last 30 days</p>
</header>
<section>
<div class="progress" data-size="xl" role="img" aria-label="Cancellations by reason: other 60%, recurring payment declined 23%, not satisfied with quality 9%, too expensive 5%, changed my mind 3%">
<span style="width: 60%"></span>
<span style="width: 23%"></span>
<span style="width: 9%"></span>
<span style="width: 5%"></span>
<span style="width: 3%"></span>
</div>
<ul>
<li><span>Other</span><span>60%</span></li>
<li><span>Recurring payment declined</span><span>23%</span></li>
<li><span>Not satisfied with quality</span><span>9%</span></li>
<li><span>Too expensive</span><span>5%</span></li>
<li><span>Changed my mind</span><span>3%</span></li>
</ul>
</section>
</div>
The page moves the bar by setting width and aria-valuenow together
Tsunami has no JS for progress. The page sets both values itself. This example uses a short inline script.
<div class="stack" data-gap="sm">
<div class="progress" id="pg-import" role="progressbar" aria-label="Importing products" aria-valuenow="10" aria-valuemin="0" aria-valuemax="100">
<span style="width: 10%"></span>
</div>
<div>
<button type="button" class="btn" data-size="sm" id="pg-import-add">Add 15%</button>
</div>
</div>
<script>
document.getElementById("pg-import-add").addEventListener("click", () => {
const bar = document.getElementById("pg-import");
const value = Math.min(100, Number(bar.getAttribute("aria-valuenow")) + 15);
bar.setAttribute("aria-valuenow", value);
bar.querySelector("span").style.width = value + "%";
});
</script>
Native <progress>
For a simple single value, you can use
<progress class="progress">. It has progressbar
semantics built in. Put data-size and
data-variant on the element.
<progress class="progress" data-variant="ok" value="40" max="100" aria-label="Uploading images">40%</progress>
Attributes
| Attribute / element | Values | Description |
|---|---|---|
.progress |
Full-width track, 6px tall, subtle fill, 4px corners. Use
on a <div> (with spans) or a
<progress>.
|
|
data-size |
sm, lg, xl (omit
for 6px)
|
On the track: sm 4px, lg 12px,
xl 32px.
|
> span |
One part of the track. Set style="width: N%".
Adjacent parts have a 2px divider. Parts without a colour
fade from --fg (positions 1-5).
|
|
> span[data-variant] |
ok, warning, info,
danger, empty (omit for --fg)
|
The part's colour. empty draws nothing and
acts as a spacer so the next part starts mid-track.
|
progress[data-variant] |
ok, warning, info,
danger
|
Colour of a native <progress>. |
role |
progressbar, img |
On the track: progressbar for a single value,
img for a bar split into parts (with an
aria-label that names every part).
|
aria-valuenow |
Number | The current value on a progressbar track. Set it together with the span's width. |
aria-valuemin |
0 |
Minimum value. |
aria-valuemax |
100 |
Maximum value. |
aria-valuetext |
Text | Optional. The value read aloud, e.g. "7.8 GB of 10 GB". |
aria-label |
Text | Name of the bar. |
aria-labelledby |
id |
Use instead of aria-label when the label is
shown in the header.
|
header + .progress |
<header> before the track. The first
child is the label, the last child is the value, muted and
right-aligned, 12px text.
|
|
.progress + ul > li |
List after the track. Each row gets the marker colour of the part in the same position. The last span in a row is the value, right-aligned and bold. |
API
Progress has no script, functions or events. The page sets the
span's style.width and
aria-valuenow (and aria-valuetext if
used) together. Width changes animate smoothly over 0.3s (no
animation with reduced motion).