State art
tier 2 — classesThe drawing that says which state a view is in, before the sentence is read. Thirteen of them ship with the package, one per state; an app adds its own with the same classes. Where the thing that is missing has a glyph of its own, the state icon takes the drawing's place.
Putting it on a page
<SednaStateArt /> once, at the top of <body> in the
host page, writes the sprite into the document; every page then references a drawing by
id with a same-document <use>. It is in the page and not a file on
purpose — a <use> pointing at an external file is rendered as a
document of its own in WebKit, with no inherited colour and no custom properties, so every
line came out black — and it arrives in the first server-rendered paint, which
matters most on the pages that show when something is broken.
aria-hidden, because the drawing repeats what the title says.
<!-- Once, at the top of <body> in the host page. It writes the sprite into the page. -->
<SednaStateArt />
<!-- Anywhere on any page: -->
<svg class="state-art state-art--lg" aria-hidden="true">
<use href="#no-access" />
</svg>
The thirteen states
Pick by what happened: #nothing-yet and #all-done are both an
empty list and are opposites; #failed came back wrong and
#offline never left; #no-results searched everything and
#filtered-out excluded it. Reusing one drawing across two states makes them
look like the same page to somebody scanning a screenshot in a ticket.
nothing-yet
nothing has been createdno-results
searched, no matchfiltered-out
the filters excluded it allno-access
it exists and is shutfailed
it came back wrongoffline
it never leftwaiting
accepted and queuedall-done
cleared, not brokenfirst-run
nothing set up yetnot-found
404, the address is wrongserver-error
500, the machine threwsession-expired
time ran outmaintenance
planned, and it comes back<div class="sedna-row-wrap sedna-gap-3" style="row-gap: var(--space-9)">
<div class="sedna-col sedna-center sedna-gap-1" style="width:150px">
<svg class="state-art" aria-hidden="true"><use href="#nothing-yet" /></svg>
<code>nothing-yet</code>
<span class="text-muted text-sm text-center">nothing has been created</span>
</div>
<div class="sedna-col sedna-center sedna-gap-1" style="width:150px">
<svg class="state-art" aria-hidden="true"><use href="#no-results" /></svg>
<code>no-results</code>
<span class="text-muted text-sm text-center">searched, no match</span>
</div>
<div class="sedna-col sedna-center sedna-gap-1" style="width:150px">
<svg class="state-art" aria-hidden="true"><use href="#filtered-out" /></svg>
<code>filtered-out</code>
<span class="text-muted text-sm text-center">the filters excluded it all</span>
</div>
<div class="sedna-col sedna-center sedna-gap-1" style="width:150px">
<svg class="state-art" aria-hidden="true"><use href="#no-access" /></svg>
<code>no-access</code>
<span class="text-muted text-sm text-center">it exists and is shut</span>
</div>
<div class="sedna-col sedna-center sedna-gap-1" style="width:150px">
<svg class="state-art" aria-hidden="true"><use href="#failed" /></svg>
<code>failed</code>
<span class="text-muted text-sm text-center">it came back wrong</span>
</div>
<div class="sedna-col sedna-center sedna-gap-1" style="width:150px">
<svg class="state-art" aria-hidden="true"><use href="#offline" /></svg>
<code>offline</code>
<span class="text-muted text-sm text-center">it never left</span>
</div>
<div class="sedna-col sedna-center sedna-gap-1" style="width:150px">
<svg class="state-art" aria-hidden="true"><use href="#waiting" /></svg>
<code>waiting</code>
<span class="text-muted text-sm text-center">accepted and queued</span>
</div>
<div class="sedna-col sedna-center sedna-gap-1" style="width:150px">
<svg class="state-art" aria-hidden="true"><use href="#all-done" /></svg>
<code>all-done</code>
<span class="text-muted text-sm text-center">cleared, not broken</span>
</div>
<div class="sedna-col sedna-center sedna-gap-1" style="width:150px">
<svg class="state-art" aria-hidden="true"><use href="#first-run" /></svg>
<code>first-run</code>
<span class="text-muted text-sm text-center">nothing set up yet</span>
</div>
<div class="sedna-col sedna-center sedna-gap-1" style="width:150px">
<svg class="state-art" aria-hidden="true"><use href="#not-found" /></svg>
<code>not-found</code>
<span class="text-muted text-sm text-center">404, the address is wrong</span>
</div>
<div class="sedna-col sedna-center sedna-gap-1" style="width:150px">
<svg class="state-art" aria-hidden="true"><use href="#server-error" /></svg>
<code>server-error</code>
<span class="text-muted text-sm text-center">500, the machine threw</span>
</div>
<div class="sedna-col sedna-center sedna-gap-1" style="width:150px">
<svg class="state-art" aria-hidden="true"><use href="#session-expired" /></svg>
<code>session-expired</code>
<span class="text-muted text-sm text-center">time ran out</span>
</div>
<div class="sedna-col sedna-center sedna-gap-1" style="width:150px">
<svg class="state-art" aria-hidden="true"><use href="#maintenance" /></svg>
<code>maintenance</code>
<span class="text-muted text-sm text-center">planned, and it comes back</span>
</div>
</div>
Three sizes
.state-art--sm (56px) for a row inside a table or a narrow panel, the bare
.state-art (96px) for a card, and .state-art--lg (152px) for a
page that is nothing but this — see Common pages. There is
no step below 56px; where there is less room than that, use the sentence on its own.
| Reference | Customer | Value |
|---|---|---|
Three filters are applied. Removing the date range would show 84 orders.
Three filters are applied to 1,204 orders. Removing the date range would show 84 of them.
<div class="sedna-col sedna-gap-2" style="max-width:640px">
<table class="table">
<thead>
<tr><th>Reference</th><th>Customer</th><th class="col-num">Value</th></tr>
</thead>
<tbody>
<tr>
<td colspan="3">
<div class="empty-state empty-state--bare">
<svg class="state-art state-art--sm" aria-hidden="true"><use href="#filtered-out" /></svg>
<span class="empty-state-title">No orders match these filters</span>
<p>Removing the date range would show 84 orders.</p>
</div>
</td>
</tr>
</tbody>
</table>
<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 empty-state--page">
<svg class="state-art state-art--lg" aria-hidden="true"><use href="#filtered-out" /></svg>
<span class="empty-state-code">No results</span>
<span class="empty-state-title">No orders match these filters</span>
<p>Three filters are applied to 1,204 orders. Removing the date range would show 84
of them.</p>
<div class="empty-state-actions">
<button class="btn btn-sm btn-primary"><i class="ri-filter-off-line"></i> Clear filters</button>
<button class="btn btn-sm"><i class="ri-calendar-line"></i> Widen the date range</button>
</div>
</div>
</div>
</div>
The accent, by meaning
Every drawing has one accent — the part that says what happened — and it is
--state-accent, the brand colour by default. .state-art--go,
--warn, --danger and --info point it at the same
families the badges and buttons use, and
.empty-state--failed points it at danger for
everything inside. The line work stays muted in all of them: a whole green drawing reads
as an illustration of green, not of a cleared queue. Switch the theme in the header and the
drawings follow; nothing here names a colour.
.state-art
the brand, by default.state-art--go
a cleared queue.state-art--warn
a role to ask for.state-art--danger
a machine that failed.state-art--info
something to try<div class="sedna-row-wrap sedna-gap-3" style="row-gap: var(--space-9)">
<div class="sedna-col sedna-center sedna-gap-1" style="width:150px">
<svg class="state-art" aria-hidden="true"><use href="#all-done" /></svg>
<code>.state-art</code>
<span class="text-muted text-sm text-center">the brand, by default</span>
</div>
<div class="sedna-col sedna-center sedna-gap-1" style="width:150px">
<svg class="state-art state-art--go" aria-hidden="true"><use href="#all-done" /></svg>
<code>.state-art--go</code>
<span class="text-muted text-sm text-center">a cleared queue</span>
</div>
<div class="sedna-col sedna-center sedna-gap-1" style="width:150px">
<svg class="state-art state-art--warn" aria-hidden="true"><use href="#no-access" /></svg>
<code>.state-art--warn</code>
<span class="text-muted text-sm text-center">a role to ask for</span>
</div>
<div class="sedna-col sedna-center sedna-gap-1" style="width:150px">
<svg class="state-art state-art--danger" aria-hidden="true"><use href="#server-error" /></svg>
<code>.state-art--danger</code>
<span class="text-muted text-sm text-center">a machine that failed</span>
</div>
<div class="sedna-col sedna-center sedna-gap-1" style="width:150px">
<svg class="state-art state-art--info" aria-hidden="true"><use href="#first-run" /></svg>
<code>.state-art--info</code>
<span class="text-muted text-sm text-center">something to try</span>
</div>
</div>
The part that moves
Seven of the drawings have a part that can move, and it is paused until the page says
otherwise: .state-art--live runs one drawing, and
.empty-state--pending runs the drawing inside it — the card at the end
— because a wait is the one state where “still happening” is the
message. Everything else stays still: a drawing beside a sentence is read, and a movement
beside it competes with the reading. Every animation's first frame is the resting pose, so
a paused drawing is the still drawing, and prefers-reduced-motion switches all
of them off, last.
nothing-yet
the first item arrivingno-results
the magnifier sweepingwaiting
the arrow at the free slotserver-error
the fault lightsession-expired
the clock tickingfirst-run
the sparkle breathingoffline
the signal that is not thereoffline
the same, without --liveThis report starts once the exports ahead of it finish, at about 12:20. You can leave the page.
<div class="sedna-col sedna-gap-3">
<div class="sedna-row-wrap sedna-gap-3" style="row-gap: var(--space-9)">
<div class="sedna-col sedna-center sedna-gap-1" style="width:150px">
<svg class="state-art state-art--live" aria-hidden="true"><use href="#nothing-yet" /></svg>
<code>nothing-yet</code>
<span class="text-muted text-sm text-center">the first item arriving</span>
</div>
<div class="sedna-col sedna-center sedna-gap-1" style="width:150px">
<svg class="state-art state-art--live" aria-hidden="true"><use href="#no-results" /></svg>
<code>no-results</code>
<span class="text-muted text-sm text-center">the magnifier sweeping</span>
</div>
<div class="sedna-col sedna-center sedna-gap-1" style="width:150px">
<svg class="state-art state-art--live" aria-hidden="true"><use href="#waiting" /></svg>
<code>waiting</code>
<span class="text-muted text-sm text-center">the arrow at the free slot</span>
</div>
<div class="sedna-col sedna-center sedna-gap-1" style="width:150px">
<svg class="state-art state-art--live state-art--danger" aria-hidden="true"><use href="#server-error" /></svg>
<code>server-error</code>
<span class="text-muted text-sm text-center">the fault light</span>
</div>
<div class="sedna-col sedna-center sedna-gap-1" style="width:150px">
<svg class="state-art state-art--live" aria-hidden="true"><use href="#session-expired" /></svg>
<code>session-expired</code>
<span class="text-muted text-sm text-center">the clock ticking</span>
</div>
<div class="sedna-col sedna-center sedna-gap-1" style="width:150px">
<svg class="state-art state-art--live" aria-hidden="true"><use href="#first-run" /></svg>
<code>first-run</code>
<span class="text-muted text-sm text-center">the sparkle breathing</span>
</div>
<div class="sedna-col sedna-center sedna-gap-1" style="width:150px">
<svg class="state-art state-art--live" aria-hidden="true"><use href="#offline" /></svg>
<code>offline</code>
<span class="text-muted text-sm text-center">the signal that is not there</span>
</div>
<div class="sedna-col sedna-center sedna-gap-1" style="width:150px">
<svg class="state-art" aria-hidden="true"><use href="#offline" /></svg>
<code>offline</code>
<span class="text-muted text-sm text-center">the same, without <code>--live</code></span>
</div>
</div>
<div class="card" style="max-width:420px">
<div class="empty-state empty-state--pending">
<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>
Drawing your own
An app's own drawing
Put a <symbol> in a hidden <svg class="sedna-sprite">
anywhere in the page — the layout, the page itself — and reference it exactly
as a shipped one. The classes are the drawing: .sedna-art-line for the line
work, .sedna-art-soft for what is there but not the point,
.sedna-art-dash for what is absent or expected, .sedna-art-accent
(and .sedna-art-accent-fill) for the one part that says what happened,
.sedna-art-tint for a faint wash behind a shape, and
.sedna-art-knockout for a shape that has to cover the line behind it and takes
the colour of whatever surface the drawing sits on. Sizing, colour, the accent families and
the motion gating are all the classes', so the paths are the only thing you write.
Draw on the same 120-unit grid, with nothing thinner than the 3-unit stroke the classes set
and no gap under 6 units — the smallest size renders at 56px, and detail below that
fills in. One accent per scene. A part that moves takes one of the motion classes,
.sedna-art-bob, .sedna-art-sweep, .sedna-art-nudge,
.sedna-art-blink, .sedna-art-tick or
.sedna-art-twinkle, and rests until .state-art--live says
otherwise. Prefix the id with something of your own, so it can never collide with a state
that ships later.
Nothing was billed between 1 and 31 August. The first invoice appears here once a delivery is confirmed.
<svg class="sedna-sprite" aria-hidden="true" focusable="false">
<symbol id="ex-no-invoices" viewBox="0 0 120 120">
<path class="sedna-art-tint" d="M30 16h40l18 18v64a6 6 0 0 1-6 6H30a6 6 0 0 1-6-6V22a6 6 0 0 1 6-6z" />
<path class="sedna-art-soft" d="M30 16h40l18 18v64a6 6 0 0 1-6 6H30a6 6 0 0 1-6-6V22a6 6 0 0 1 6-6z" />
<path class="sedna-art-soft" d="M70 16v18h18" />
<path class="sedna-art-soft" d="M38 50h32M38 62h32M38 74h20" />
<circle class="sedna-art-knockout" cx="86" cy="86" r="24" />
<g class="sedna-art-bob">
<circle class="sedna-art-accent" cx="86" cy="86" r="18" />
<path class="sedna-art-accent" d="M78 86h16" />
</g>
</symbol>
</svg>
<div class="card" style="max-width:420px">
<div class="empty-state">
<svg class="state-art state-art--live" aria-hidden="true"><use href="#ex-no-invoices" /></svg>
<span class="empty-state-title">No invoices for this period</span>
<p>Nothing was billed between 1 and 31 August. The first invoice appears here once a
delivery is confirmed.</p>
<button class="btn btn-sm"><i class="ri-calendar-line"></i> Change the period</button>
</div>
</div>
CanvasText, the accent Highlight and a knockout
Canvas; on paper the drawing prints as it stands, in the light palette. The
sprite writes no colour, which is what makes both true without a second file.