Progress and spinners
tier 2 — classesThree ways to say “working”, picked by how much you know about what is coming. The fourth — the shape of what is coming, before it arrives — is a skeleton.
.progress bar. No
percentage, but a known shape — a table of rows, a card — is a
.skeleton. Neither is a .spinner. Nothing at all yet, in a panel that will fill, is
.empty-state--pending.
Busy, on a control and on a surface
aria-busy="true" is the one switch, and the paint follows what is announced. On a
control — a menu item, a tab, a list row, a chip, a page link — its leading icon becomes the
spinner and it takes no pointer, so the work cannot start twice; a .btn covers its
label instead. On a surface — a card, a list, a table, a dialog, a drawer — the content stays
in place, dimmed and out of reach, with a bar along the top edge. Under reduced motion both
stand still.
Open orders
ORD-4182, ORD-4183 and two more, as they were a moment ago.
<div class="sedna-col sedna-gap-3" style="max-width:420px">
<div class="menu" style="position:static">
<button class="menu-item" type="button" aria-busy="true"><i class="ri-refresh-line"></i> <span>Refresh the list</span></button>
<button class="menu-item" type="button"><i class="ri-download-2-line"></i> <span>Export</span></button>
</div>
<div class="card" aria-busy="true">
<div class="card-head"><h3>Open orders</h3></div>
<div class="card-body"><p>ORD-4182, ORD-4183 and two more, as they were a moment ago.</p></div>
</div>
</div>
Progress bar
Determinate: set the inline width on .progress-bar.
Indeterminate: leave the width off and add .progress--indeterminate to the
track. Put the number in text as well — a bar is a shape, and a reader deciding whether to
wait needs the figure.
Uploading — 72%
Complete
Slower than expected — still running
Stopped at 34% — quota exceeded
Working — no total known
<div style="display:flex; flex-direction:column; gap:14px; max-width:420px">
<div>
<div class="progress"><div class="progress-bar" style="width:72%"></div></div>
<p class="form-hint">Uploading — 72%</p>
</div>
<div>
<div class="progress"><div class="progress-bar progress-bar--go" style="width:100%"></div></div>
<p class="form-hint">Complete</p>
</div>
<div>
<div class="progress"><div class="progress-bar progress-bar--warn" style="width:72%"></div></div>
<p class="form-hint">Slower than expected — still running</p>
</div>
<div>
<div class="progress"><div class="progress-bar progress-bar--danger" style="width:34%"></div></div>
<p class="form-hint">Stopped at 34% — quota exceeded</p>
</div>
<div>
<div class="progress progress--indeterminate"><div class="progress-bar"></div></div>
<p class="form-hint">Working — no total known</p>
</div>
</div>
Measuring towards a threshold
.progress-bar--muted is for a bar measuring progress towards a
threshold rather than the completion of a task, where well below the line is neither a
warning nor a failure. The base bar is var(--brand), which reads as an alarm
wherever the brand is a red.
Password reset — 94% sent unchanged, past the 90% threshold
Group membership — 86%, close enough to watch
Licence assignment — 31%, nowhere near it yet
<div style="display:flex; flex-direction:column; gap:14px; max-width:420px">
<div>
<div class="progress"><div class="progress-bar progress-bar--go" style="width:94%"></div></div>
<p class="form-hint">Password reset — 94% sent unchanged, past the 90% threshold</p>
</div>
<div>
<div class="progress"><div class="progress-bar progress-bar--warn" style="width:86%"></div></div>
<p class="form-hint">Group membership — 86%, close enough to watch</p>
</div>
<div>
<div class="progress"><div class="progress-bar progress-bar--muted" style="width:31%"></div></div>
<p class="form-hint">Licence assignment — 31%, nowhere near it yet</p>
</div>
</div>
Ranked bars
.progress-bar--sev-1 … --sev-5 take the ordered ramp behind the
severity badges, for a bar that measures a rank rather than a fraction.
They are a fill and carry no label — where there is one, use .badge-sev-N.
P1 — 4 of 51 open incidents
P2 — 11 of 51
P3 — 23 of 51
P4 — 9 of 51
P5 — 4 of 51
<div style="display:flex; flex-direction:column; gap:10px; max-width:420px">
<div>
<div class="progress"><div class="progress-bar progress-bar--sev-1" style="width:8%"></div></div>
<p class="form-hint">P1 — 4 of 51 open incidents</p>
</div>
<div>
<div class="progress"><div class="progress-bar progress-bar--sev-2" style="width:22%"></div></div>
<p class="form-hint">P2 — 11 of 51</p>
</div>
<div>
<div class="progress"><div class="progress-bar progress-bar--sev-3" style="width:45%"></div></div>
<p class="form-hint">P3 — 23 of 51</p>
</div>
<div>
<div class="progress"><div class="progress-bar progress-bar--sev-4" style="width:18%"></div></div>
<p class="form-hint">P4 — 9 of 51</p>
</div>
<div>
<div class="progress"><div class="progress-bar progress-bar--sev-5" style="width:8%"></div></div>
<p class="form-hint">P5 — 4 of 51</p>
</div>
</div>
Spinner
Sized in em, so it matches whatever text or button it sits in without a second
class. .spinner--lg is the standalone size for a panel that is still loading.
<span class="text-sm">Saving draft <span class="spinner spinner--sm"></span></span>
<span>Checking the connection <span class="spinner"></span></span>
<button class="btn" disabled><span class="spinner"></span> <span>Sending…</span></button>
<button class="btn btn--brand btn--solid" disabled><span class="spinner"></span> <span>Saving</span></button>
<span class="spinner spinner--lg"></span>
Progress ring
.progress-ring is the determinate value where there is no width to fill.
Write the value as a percentage in --progress and the same number in
aria-valuenow; the element holds a label if you give it one.
--progress-ring-size and --progress-ring-width size it, and
.progress-ring--lg is the larger step and .progress-ring--sm the smaller, beside a line of text. .spinner--sm is three quarters of the text it sits in.
<div class="sedna-row sedna-gap-3">
<div class="progress-ring" role="progressbar" aria-label="Uploading invoice scans" aria-valuemin="0" aria-valuemax="100" aria-valuenow="25" style="--progress: 25%"></div>
<div class="progress-ring" role="progressbar" aria-label="Importing orders" aria-valuemin="0" aria-valuemax="100" aria-valuenow="62" style="--progress: 62%">62%</div>
<div class="progress-ring progress-ring--lg" role="progressbar" aria-label="Syncing the catalogue" aria-valuemin="0" aria-valuemax="100" aria-valuenow="90" style="--progress: 90%">90%</div>
</div>
Round ends, and a ring that glides
.progress-ring--round rounds both ends of the arc, in every size. Every ring
glides from one value to the next, as a .progress-bar does, and lands at once
under reduced motion — so write each new value as it comes and leave the animating to
the stylesheet. Blazor's own --blazor-load-percentage works as the value while
WebAssembly starts.
<div class="sedna-row sedna-gap-3">
<div class="progress-ring progress-ring--round progress-ring--sm" role="progressbar" aria-label="Checking the address list" aria-valuemin="0" aria-valuemax="100" aria-valuenow="40" style="--progress: 40%"></div>
<div class="progress-ring progress-ring--round" role="progressbar" aria-label="Importing orders" aria-valuemin="0" aria-valuemax="100" aria-valuenow="62" style="--progress: 62%">62%</div>
<div class="progress-ring progress-ring--round progress-ring--lg" role="progressbar" aria-label="Starting the app" aria-valuemin="0" aria-valuemax="100" aria-valuenow="18" style="--progress: var(--blazor-load-percentage, 18%)">18%</div>
</div>
A splash while the app starts
.splash covers the viewport in the theme's background while the app starts,
clear of the notch. Put it inside the element the app renders into, so the first render
replaces it. Blazor WebAssembly writes --blazor-load-percentage on the
document while it downloads, so a ring fed from it fills as the app loads.
<!-- In the host page, inside the element the app renders into, so the app replaces it. -->
<div id="app">
<div class="splash" role="status">
<i class="ri-dashboard-3-line" aria-hidden="true" style="font-size: var(--text-11)"></i>
<div class="progress-ring progress-ring--lg" style="--progress: var(--blazor-load-percentage, 0%)"></div>
<span>Starting the console…</span>
</div>
</div>
A list of running work
Where these end up in practice: one row per job, with the shape that matches what is known —
a percentage gets a determinate bar, a wait with no total gets the indeterminate one and a
.spinner, and anything finished gets a badge instead. Add
aria-busy="true" to the row while it is running, and put the figure in text as
well as in the bar.
- build 4182 — orders-api Compiling · started 2 min ago
- deploy 4181 — EU-West Waiting for the health check — no total known
- deploy 4180 — EU-Central Rolled back at 34% — quota exceeded
- deploy 4179 — UK-South Live since 08:41
<ul class="list" style="max-width:520px">
<li>
<div class="list-row">
<i class="ri-git-commit-line"></i>
<span class="list-main">
<span class="list-title">build 4182 — orders-api</span>
<span class="list-sub">Compiling · started 2 min ago</span>
<span class="progress sedna-mt-1"><span class="progress-bar" style="width:64%"></span></span>
</span>
<span class="list-meta text-nums">64%</span>
</div>
</li>
<li>
<div class="list-row">
<i class="ri-rocket-line"></i>
<span class="list-main">
<span class="list-title">deploy 4181 — EU-West</span>
<span class="list-sub">Waiting for the health check — no total known</span>
<span class="progress progress--indeterminate sedna-mt-1"><span class="progress-bar"></span></span>
</span>
<span class="list-meta"><span class="spinner"></span></span>
</div>
</li>
<li>
<div class="list-row">
<i class="ri-close-circle-line"></i>
<span class="list-main">
<span class="list-title">deploy 4180 — EU-Central</span>
<span class="list-sub">Rolled back at 34% — quota exceeded</span>
<span class="progress sedna-mt-1"><span class="progress-bar progress-bar--danger" style="width:34%"></span></span>
</span>
<span class="list-meta"><span class="badge badge--danger">Failed</span></span>
</div>
</li>
<li>
<div class="list-row">
<i class="ri-check-double-line"></i>
<span class="list-main">
<span class="list-title">deploy 4179 — UK-South</span>
<span class="list-sub">Live since 08:41</span>
<span class="progress sedna-mt-1"><span class="progress-bar progress-bar--go" style="width:100%"></span></span>
</span>
<span class="list-meta"><span class="badge badge--go">Done</span></span>
</div>
</li>
</ul>