- ds
- Components
- Spinner and progress
Spinner and progress
Loading states that say how long, when they can.
Spinner
Loading tables…
Code
<span class="spin sm" role="status" aria-label="Loading"></span>
<span class="spin" role="status" aria-label="Loading"></span>
<span class="spin lg" role="status" aria-label="Loading"></span>
<span class="spin xl" role="status" aria-label="Loading"></span>
<span class="spin muted" role="status" aria-label="Loading"></span>
<span class="loading"><span class="spin sm muted"></span>Loading tables…</span>A ring with one bright quarter, in the current colour, turning at 0.7 seconds. Use it when you cannot say how long something will take. Under reduced motion it slows down rather than stopping, because a frozen spinner looks like a hang.
In buttons
Code
<button class="btn primary" aria-busy="true"><span class="spin sm"></span>Deploying</button>
<button class="btn" aria-busy="true"><span class="spin sm"></span>Testing connection</button>
<button class="btn sm ghost" aria-busy="true"><span class="spin sm"></span>Refreshing</button>Swap the icon for a spinner and the verb for its -ing form, and set aria-busy. The button keeps its width close to what it was, and people can see which action is running.
Progress
Backfilling orders, 2024-01-01 to 2026-10-12
412 of 665 partitions · about 9 minutes left
Rebuilding search index
Counting documents
Exporting events_raw
Stopped at partition 2025-03-14: disk quota exceeded on /scratch
Show code
<div class="progress">
<b>Backfilling orders, 2024-01-01 to 2026-10-12</b><output>62%</output>
<div class="meter" role="progressbar" aria-valuenow="62" aria-valuemin="0" aria-valuemax="100" aria-label="Backfill" style="--v:.62"><i></i></div>
<small>412 of 665 partitions · about 9 minutes left</small>
</div>
<div class="progress">
<b>Rebuilding search index</b><output>…</output>
<div class="meter indeterminate" role="progressbar" aria-label="Rebuilding search index"><i></i></div>
<small>Counting documents</small>
</div>
<div class="progress">
<b>Exporting events_raw</b><output>18%</output>
<div class="meter down" role="progressbar" aria-valuenow="18" aria-valuemin="0" aria-valuemax="100" aria-label="Export" style="--v:.18"><i></i></div>
<small class="err">Stopped at partition 2025-03-14: disk quota exceeded on /scratch</small>
</div>When you can count, show the count. A label, a figure, the meter and one line of what is left in units people know: partitions, rows, files, minutes. Start indeterminate while counting, then switch to the real number.
Uploads
- orders_2026_09.parquet48 / 112 MB
- customers.csv2.1 MBUploaded, 18,204 rows
- refunds.xlsx0.4 MBSpreadsheets are not supported. Export it as CSV.
Code
<ul class="uploads">
<li class="upload"><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/></svg><div class="progress"><b>orders_2026_09.parquet</b><span class="v">48 / 112 MB</span><div class="meter" style="--v:.43"><i></i></div></div><button class="btn icon xs ghost" aria-label="Cancel upload"><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button></li>
<li class="upload ok"><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/></svg><div class="progress"><b>customers.csv</b><span class="v">2.1 MB</span><small>Uploaded, 18,204 rows</small></div><button class="btn icon xs ghost" aria-label="Remove"><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="M10 11v6"/><path d="M14 11v6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M3 6h18"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg></button></li>
<li class="upload err"><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/></svg><div class="progress"><b>refunds.xlsx</b><span class="v">0.4 MB</span><small class="err">Spreadsheets are not supported. Export it as CSV.</small></div><button class="btn xs" >Retry</button></li>
</ul>API
| Class | On | What it does |
|---|---|---|
.spin | span | The spinner, 1rem, in the accent. Give it role="status" and a label when it stands alone. |
.sm / .lg / .xl | .spin | 0.75, 1.5 and 2.25rem. |
.muted | .spin | Grey, for secondary loading. |
aria-busy="true" | .btn | A button doing its job. Put a .spin inside. |
.loading | span | A spinner and a line of text. .block centres it in a panel. |
.progress | div | Label in <b>, figure in <output>, a .meter, a <small> line. |
.uploads / .upload | ul / li | One row per file: icon, .progress, an action. .ok and .err. |
Do
- A skeleton for content with a known shape, a spinner for the rest.
- Time left when it is more than a few seconds.
- Say what is loading.
Don't
- A spinner for under 300 ms. It only flickers.
- Full-page spinners while part of the page could show.
- Percentages that jump back.
Accessibility
- A spinner on its own needs
role="status"and a label. Inside a busy button, the button's text says it. - Give progress bars
role="progressbar"witharia-valuenow, and leave out the value while indeterminate.