States

Empty, loading, and error states — the screens a component set never shows you.

A component set shows every part working. A product spends a lot of its life in the three states where nothing is working yet: there is no data, the data has not arrived, and the data will not arrive. Those three screens are where a design system is actually tested, so they are specified here rather than left to whoever hits them first.

Every state on this page follows the same shape:

What is truewhythe one thing to do about it.

If a state cannot name the one thing to do, it is an error state with an optimistic title.

Empty states

Three kinds, and they are not interchangeable:

KindWhenThe action
First runThe feature works; nothing has been created yetThe accent button that creates the first one
Filtered to nothingThere is data; the current filter excludes all of itClear the filter — and show what the filter is
Nothing to reportEmpty is the good outcome — no open findingsNo action. Say so plainly

The first-run empty state is the only one that gets an accent button — it is the only one where creating something is what the user came to do. “Filtered to nothing” gets a ghost button, because the fix is to undo, not to create.

First run, filtered to nothing, nothing to report
No pipelines yet
A pipeline runs your policy checks before a deployment is promoted.
No pipelines match
48 pipelines exist. None are both strict and failing.
No open findings
The last audit completed 4 minutes ago and found nothing.
Healthy

An empty state is body text, not an illustration. The system’s answer to a blank screen is a diagram or nothing, never a mascot or a spot illustration of a person holding a magnifying glass.

Loading states

Two mechanisms, chosen by what you know about the shape of what is coming:

UseWhy
SkeletonYou know the layout — a table of rows, a card grid, a KPI rowThe layout does not jump when content arrives
SpinnerYou know nothing — an action in flight, an indeterminate waitThere is no shape to promise

Prefer the skeleton. A spinner in the middle of a region that is about to become a table tells the user nothing and then reflows the page underneath them.

A skeleton is midnight-2 blocks at the real dimensions of the content, with the real gaps and the real number of rows where that is known. It carries a 100ms shimmer at --duration-standard, and it does not shimmer at all under prefers-reduced-motion: reduce — it simply sits there as static blocks.

Skeleton — real dimensions, real row count
Spinner — indeterminate work, with its label
Applying changes…

Timing rules, so the loading state does not become its own flicker:

  • Under 200ms — show nothing. A skeleton that appears and vanishes is worse than a brief pause.
  • 200ms to 10s — skeleton or spinner.
  • Over 10s — a determinate pj-progress bar with a count (“3 of 12 checks”), or the state moves to a background job with its own row in the list.

Every loading region carries role="status" and an accessible label, or a screen-reader user gets silence where a sighted user gets motion. Never animate a skeleton in a way that hides that the operation has stalled — a shimmer that runs for two minutes says “working” when the truth is “stuck”.

Error pages

Full-page errors — 404 and 500 — are the brand’s worst-case first impression, so they use the same page shell and the same voice as everything else. No apology paragraph, no cartoon, no error code as a headline.

404500
Headline“That page does not exist”“Something failed on our side”
BodyWhat might have happened, in one sentenceWhat we know, and whether it is being worked on
ActionBack to a real destination + searchRetry, and a route to a status page or support
BlameThe link, not the readerUs, explicitly

The code (404, 500) appears as an overline above the headline, in the muted foreground — findable when someone is reporting the problem, never the loudest thing on the page.

404 and 500 — same shell, different obligation
Error 404
That page does not exist
The link may be out of date, or the pipeline may have been deleted.
Error 500
Something failed on our side
The request did not complete. Nothing was deployed. Reference req_8f2c14.

A 500 says what did not happen. “Nothing was deployed” is the sentence the reader needs; “an unexpected error occurred” is the sentence they cannot act on. If the outcome is genuinely unknown, say that instead — it is a different fact and it changes what they do next.

Inline errors

Not every failure takes the page. A failure scoped to one region stays in that region, so the rest of the screen remains usable:

  • A field fails with pj-input--error and a message below it — never a tooltip, never colour on the border alone.
  • A panel fails with a pj-alert--danger inside the panel and a retry control, while the surrounding page keeps working.
  • A background job fails into a row in the activity feed with a pj-status --err dot and the word “Failed”.
Panel-scoped failure — the page keeps working
Agent activity
Could not load activity
The activity service did not respond. Everything else on this page is current.
Do

Say what is true, why, and the one thing to do. Use a skeleton wherever the layout is known. Label every loading region for screen readers. Tell a reader what did not happen when something failed.

Don't

Show “no results” before the response arrives, put an accent button on a “filtered to nothing” state, headline an error with its status code, or let a region-scoped failure take the whole page.