Empty and live states
tier 2 — classesWhat a view shows when it has nothing to show — and what it shows while it is waiting, streaming or disconnected. Say which of those it is: they call for different actions, and showing the wrong one is how a broken page looks fine.
Empty
Four kinds of empty, and they are not interchangeable. Nothing yet is an invitation. Nothing found needs the query echoed back so the reader can see what was searched. Filtered to nothing needs the filters named and a way to clear them. No access needs to say who can see it and how to ask.
Give the one action that resolves it, and no others. Inside a .card for a panel,
or .empty-state--bare straight on the page.
The first order will appear here as soon as one is placed.
Check the spelling, or clear the search to see all 1,204 orders.
Three filters are applied. Removing the date range would show 84 orders.
Regional figures are limited to account leads. Ask an administrator for the
orders.read role.
<div class="dr-col dr-gap-2" style="max-width:520px">
<div class="card">
<div class="empty-state">
<i class="ri-inbox-line"></i>
<span class="empty-state-title">No orders yet</span>
<p>The first order will appear here as soon as one is placed.</p>
</div>
</div>
<div class="card">
<div class="empty-state">
<i class="ri-search-line"></i>
<span class="empty-state-title">Nothing matches “eu-wets”</span>
<p>Check the spelling, or clear the search to see all 1,204 orders.</p>
<button class="btn btn-sm"><i class="ri-close-circle-line"></i> Clear search</button>
</div>
</div>
<div class="card">
<div class="empty-state">
<i class="ri-filter-off-line"></i>
<span class="empty-state-title">No orders match these filters</span>
<p>Three filters are applied. Removing the date range would show 84 orders.</p>
<button class="btn btn-sm"><i class="ri-filter-off-line"></i> Clear filters</button>
</div>
</div>
<div class="card">
<div class="empty-state">
<i class="ri-lock-line"></i>
<span class="empty-state-title">You cannot see this region</span>
<p>Regional figures are limited to account leads. Ask an administrator for the
<code>orders.read</code> role.</p>
</div>
</div>
</div>
Waiting, and failed
.empty-state--pending is a wait with nothing to report: the first event of a
stream, a long request. Put a .spinner-lg where the icon goes. Where there is
progress use a .progress bar; where the shape of what is
coming is known, use a .skeleton.
.empty-state--failed colours the icon only. A whole red panel reads as
destructive rather than as unavailable. .empty-state-detail is the line that
makes a failure actionable — the endpoint, the code, the time it was tried — and it belongs
there rather than in a console nobody has open.
The stream is connected. Nothing has come through yet.
The request did not complete. Nothing has been changed.
GET /api/orders · 504 · 12:04:31<div class="dr-col dr-gap-2" style="max-width:520px">
<div class="card">
<div class="empty-state empty-state--pending">
<span class="spinner spinner-lg"></span>
<span class="empty-state-title">Waiting for the first event</span>
<p>The stream is connected. Nothing has come through yet.</p>
</div>
</div>
<div class="card">
<div class="empty-state empty-state--failed">
<i class="ri-cloud-off-line"></i>
<span class="empty-state-title">Could not load the orders</span>
<p>The request did not complete. Nothing has been changed.</p>
<span class="empty-state-detail">GET /api/orders · 504 · 12:04:31</span>
<button class="btn btn-sm"><i class="ri-refresh-line"></i> Try again</button>
</div>
</div>
</div>
Connection state
.health-badge with a .health-dot, in six states. The three active
ones — --live, --streaming, --connecting — pulse; the
three settled ones — --idle, --degraded, --down — do
not.
The word is not optional. A colour alone fails for anybody who cannot see
it, and green does not say whether it means connected, healthy or finished. Under
prefers-reduced-motion the pulse stops and the dot keeps its colour.
.stale is how old the number beside it is, for anything polled or pushed.
--warn when it is old enough to distrust; what counts as old is the app's
judgement.
<div class="dr-col dr-gap-2">
<div class="dr-row-wrap dr-gap-3">
<span class="health-badge health-badge--live"><span class="health-dot"></span> Live</span>
<span class="health-badge health-badge--streaming"><span class="health-dot"></span> Streaming</span>
<span class="health-badge health-badge--connecting"><span class="health-dot"></span> Connecting</span>
<span class="health-badge health-badge--idle"><span class="health-dot"></span> Idle</span>
<span class="health-badge health-badge--degraded"><span class="health-dot"></span> Degraded</span>
<span class="health-badge health-badge--down"><span class="health-dot"></span> Disconnected</span>
</div>
<div class="dr-row-wrap dr-gap-3">
<span class="stale"><i class="ri-time-line"></i> updated 4s ago</span>
<span class="stale stale--warn"><i class="ri-time-line"></i> updated 6m ago</span>
</div>
</div>
Output pane
.output is lines of machine output: a build log, a command's stdout, an event
stream, an audit trail. A real list, so the count is announced and a screen reader can walk
it. Monospace with tabular figures, because the reader is comparing columns down the page
rather than reading prose.
Add data-follow and it sticks to the newest line. It releases
the moment the reader scrolls up to read something, and re-attaches when they scroll back
down. Without the release, reading anything in a live stream is impossible; without the
re-attach, it never resumes. drSimpleUi.output.follow(pane) re-attaches it from
a “jump to latest” button.
.output-head sits outside the pane so it does not scroll away with the lines.
Severity tints the whole line rather than marking its start: the reader is scanning a wall of
text, and a single coloured word is easy to miss in one.
- 12:04:28 reading manifest for ORD-4182
- 12:04:28 5 lines, 3 warehouses
- 12:04:29 line 1 reserved in EU-West
- 12:04:29 line 2 reserved in EU-West
- 12:04:30 line 3 awaiting stock, backordered
- 12:04:30 line 4 reserved in EU-Central
- 12:04:31 line 5 awaiting stock, backordered
- 12:04:31 reservation service returned 504
- 12:04:33 retrying in 2s
- 12:04:35 reservation confirmed
- 12:04:35 done, 3 of 5 lines reserved
<div style="max-width:640px">
<div class="output-head">
<span class="health-badge health-badge--streaming"><span class="health-dot"></span> Streaming</span>
<span class="dr-push stale"><i class="ri-time-line"></i> updated 1s ago</span>
</div>
<ul class="output" data-follow tabindex="0" aria-label="Import output">
<li class="output-line"><span class="output-time">12:04:28</span> reading manifest for ORD-4182</li>
<li class="output-line"><span class="output-time">12:04:28</span> 5 lines, 3 warehouses</li>
<li class="output-line output-line--go"><span class="output-time">12:04:29</span> line 1 reserved in EU-West</li>
<li class="output-line output-line--go"><span class="output-time">12:04:29</span> line 2 reserved in EU-West</li>
<li class="output-line output-line--warn"><span class="output-time">12:04:30</span> line 3 awaiting stock, backordered</li>
<li class="output-line output-line--go"><span class="output-time">12:04:30</span> line 4 reserved in EU-Central</li>
<li class="output-line output-line--warn"><span class="output-time">12:04:31</span> line 5 awaiting stock, backordered</li>
<li class="output-line output-line--error"><span class="output-time">12:04:31</span> reservation service returned 504</li>
<li class="output-line output-line--note"><span class="output-time">12:04:33</span> retrying in 2s</li>
<li class="output-line output-line--go"><span class="output-time">12:04:35</span> reservation confirmed</li>
<li class="output-line"><span class="output-time">12:04:35</span> done, 3 of 5 lines reserved</li>
</ul>
</div>
Work in progress
Background work the reader can see and cancel. There is no activity-row class: it is a
.list-row with a
.progress bar in its subtext and the controls in
.list-meta.
A determinate bar for work with a known size, an indeterminate one for work that has been accepted but not started. Give the cancel button the job's name — a page of buttons all labelled “Cancel” is a page where cancelling is a guess.
- Reconciling EU-West
- Rebuilding the search index
<ul class="list" style="max-width:520px">
<li>
<span class="list-row">
<span class="list-main">
<span class="list-title">Reconciling EU-West</span>
<span class="list-sub">
<span class="progress" style="margin-top:6px">
<span class="progress-bar" style="width:62%"></span>
</span>
</span>
</span>
<span class="list-meta">
<span class="text-nums">62%</span>
<button class="btn btn-sm btn-icon btn-ghost" type="button" aria-label="Cancel reconciling EU-West">
<i class="ri-close-line"></i>
</button>
</span>
</span>
</li>
<li>
<span class="list-row">
<span class="list-main">
<span class="list-title">Rebuilding the search index</span>
<span class="list-sub">
<span class="progress progress--indeterminate" style="margin-top:6px">
<span class="progress-bar"></span>
</span>
</span>
</span>
<span class="list-meta">
<span class="health-badge health-badge--connecting"><span class="health-dot"></span> Queued</span>
</span>
</span>
</li>
</ul>