Connection lost. Reconnecting… attempt 1 of 8
Paused. Your work is held on the server.
Could not reconnect.
This session has expired on the server.
An unhandled error has occurred.
Sedna.UI
v0.31.0 · main

Progress and spinners

tier 2 — classes

Three 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.

Pick by what you know A percentage is a .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.

Saving draft Checking the connection
<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.

62%
90%
<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.

62%
18%
<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 64%
  • deploy 4181 — EU-West Waiting for the health check — no total known
  • deploy 4180 — EU-Central Rolled back at 34% — quota exceeded Failed
  • deploy 4179 — UK-South Live since 08:41 Done
<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 &mdash; 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 &mdash; EU-West</span>
                <span class="list-sub">Waiting for the health check &mdash; 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 &mdash; EU-Central</span>
                <span class="list-sub">Rolled back at 34% &mdash; 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 &mdash; 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>