Empty states
tier 2 — classesWhat a view shows when it has nothing to show, and what it shows while it is waiting. Say which of those it is: they call for different actions. Something still happening — a connection, a log being written, a job running — is a live state.
The drawing above each block is state art: thirteen shipped, one per state, referenced by id. That page has the set, the sizes, the accents and the motion.
Or an icon, when what is missing has one
State icon
.state-icon on a Remix Icon <i> takes the drawing's place,
with the same title, sentence and actions under it. Use it where the thing that is missing
has a glyph of its own — invoices, attachments, members — and the drawing where
the state is what matters. Pick the glyph for that thing: one inbox on every empty panel
says nothing the title does not. .empty-state--failed turns it to the danger
colour.
An invoice appears here once an order has been delivered and billed.
Upload the delivery note and the signed contract, or ask the customer to send them.
Orders in it wait until somebody is. A team lead can assign members.
The request did not complete. Nothing has been changed.
GET /api/orders/10482/files · 503 · 12:04:31<div class="sedna-col sedna-gap-2" style="max-width:520px">
<div class="card">
<div class="empty-state">
<i class="ri-file-list-3-line state-icon" aria-hidden="true"></i>
<span class="empty-state-title">No invoices yet</span>
<p>An invoice appears here once an order has been delivered and billed.</p>
<button class="btn btn-sm btn-primary"><i class="ri-add-line"></i> Create an invoice</button>
</div>
</div>
<div class="card">
<div class="empty-state">
<i class="ri-attachment-2 state-icon" aria-hidden="true"></i>
<span class="empty-state-title">No attachments on this order</span>
<p>Upload the delivery note and the signed contract, or ask the customer to send them.</p>
<div class="empty-state-actions">
<button class="btn btn-sm btn-primary"><i class="ri-upload-2-line"></i> Upload</button>
<button class="btn btn-sm"><i class="ri-mail-line"></i> Ask the customer</button>
</div>
</div>
</div>
<div class="card">
<div class="empty-state">
<i class="ri-team-line state-icon" aria-hidden="true"></i>
<span class="empty-state-title">Nobody is assigned to this queue</span>
<p>Orders in it wait until somebody is. A team lead can assign members.</p>
</div>
</div>
<div class="card">
<div class="empty-state empty-state--failed">
<i class="ri-error-warning-line state-icon" aria-hidden="true"></i>
<span class="empty-state-title">Could not load the attachments</span>
<p>The request did not complete. Nothing has been changed.</p>
<span class="empty-state-detail">GET /api/orders/10482/files · 503 · 12:04:31</span>
<button class="btn btn-sm"><i class="ri-refresh-line"></i> Try again</button>
</div>
</div>
</div>
Icon sizes
The same three places as the drawing: .state-icon--sm in a table row or a
narrow panel, the bare .state-icon in a card, and .state-icon--lg
in .empty-state--page.
| Shipment | Carrier | Parcels |
|---|---|---|
The order is packed and waiting for a carrier.
The order is packed and waiting for a carrier. Book one here, or hand it to the customer for collection.
<div class="sedna-col sedna-gap-2" style="max-width:640px">
<table class="table">
<thead>
<tr><th>Shipment</th><th>Carrier</th><th class="col-num">Parcels</th></tr>
</thead>
<tbody>
<tr>
<td colspan="3">
<div class="empty-state empty-state--bare">
<i class="ri-truck-line state-icon state-icon--sm" aria-hidden="true"></i>
<span class="empty-state-title">No shipments for this order</span>
<p>A shipment appears once the warehouse has packed the order.</p>
</div>
</td>
</tr>
</tbody>
</table>
<div class="card">
<div class="empty-state">
<i class="ri-truck-line state-icon" aria-hidden="true"></i>
<span class="empty-state-title">No shipments for this order</span>
<p>The order is packed and waiting for a carrier.</p>
<button class="btn btn-sm btn-primary"><i class="ri-add-line"></i> Book a shipment</button>
</div>
</div>
<div class="card">
<div class="empty-state empty-state--page">
<i class="ri-truck-line state-icon state-icon--lg" aria-hidden="true"></i>
<span class="empty-state-code">Shipments</span>
<span class="empty-state-title">No shipments for this order</span>
<p>The order is packed and waiting for a carrier. Book one here, or hand it to the
customer for collection.</p>
<div class="empty-state-actions">
<button class="btn btn-sm btn-primary"><i class="ri-add-line"></i> Book a shipment</button>
<button class="btn btn-sm"><i class="ri-calendar-line"></i> Schedule a collection</button>
</div>
</div>
</div>
</div>
Empty and waiting
Empty
The kinds of empty are not interchangeable: nothing yet is an invitation, nothing found
needs the query echoed back, filtered to nothing needs the filters named and a way to clear
them, no access needs to say who can grant it, and a cleared queue has to look like success.
Give the one action that resolves it as a .btn-sm, 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.
Every order in the queue has been decided. The next one will arrive here.
<div class="sedna-col sedna-gap-2" style="max-width:520px">
<div class="card">
<div class="empty-state">
<svg class="state-art" aria-hidden="true"><use href="#nothing-yet" /></svg>
<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">
<svg class="state-art" aria-hidden="true"><use href="#no-results" /></svg>
<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">
<svg class="state-art" aria-hidden="true"><use href="#filtered-out" /></svg>
<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">
<svg class="state-art" aria-hidden="true"><use href="#no-access" /></svg>
<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 class="card">
<div class="empty-state">
<svg class="state-art" aria-hidden="true"><use href="#all-done" /></svg>
<span class="empty-state-title">The queue is clear</span>
<p>Every order in the queue has been decided. The next one will arrive here.</p>
</div>
</div>
</div>
Waiting, and failed
.empty-state--pending is a wait with nothing to report, so put a
.spinner-lg where the drawing goes — use
.progress where there is progress, a
.skeleton where the shape of what is coming is known, and the
#waiting drawing where the reader can be told and then leave.
.empty-state--failed moves --state-accent into the danger ramp and
leaves the line work grey; put the endpoint, the code and the time it was tried in
.empty-state-detail, and keep #failed and #offline
apart — only a request that never left is worth retrying immediately.
The stream is connected. Nothing has come through yet.
This report starts once the exports ahead of it finish, at about 12:20. You can leave the page.
The request did not complete. Nothing has been changed.
GET /api/orders · 504 · 12:04:31The request never left this device. What you have already decided is saved and will be sent when the connection is back.
<div class="sedna-col sedna-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">
<svg class="state-art" aria-hidden="true"><use href="#waiting" /></svg>
<span class="empty-state-title">Queued behind two other exports</span>
<p>This report starts once the exports ahead of it finish, at about 12:20. You can
leave the page.</p>
</div>
</div>
<div class="card">
<div class="empty-state empty-state--failed">
<svg class="state-art" aria-hidden="true"><use href="#failed" /></svg>
<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 class="card">
<div class="empty-state empty-state--failed">
<svg class="state-art" aria-hidden="true"><use href="#offline" /></svg>
<span class="empty-state-title">You are offline</span>
<p>The request never left this device. What you have already decided is saved and
will be sent when the connection is back.</p>
<button class="btn btn-sm"><i class="ri-refresh-line"></i> Try again</button>
</div>
</div>
</div>
When the whole page is the state
.empty-state--page is the same block at page scale: a bigger drawing
(.state-art--lg), a larger title, and a wider measure.
.empty-state-code is the status code above the title, small and monospace;
.empty-state-actions is the wrapped, centred row for one primary action and the
one or two beside it, and takes the top margin off the buttons.
Common pages has the finished ones — access denied, not found, the error, the expired session, the maintenance window — with the shell each belongs in.