busy-office-ui
Menu

Pattern: error pages

Who uses it: every user, eventually — a stale bookmark, a link shared before a record moved, a permission that changed since yesterday, a request that failed server-side. How often: rare per user, universal per app — every ERP hits all three of 404, 403 and 500 sooner or later. What "done" looks like: the reader is never at a dead end — one clear sentence for what happened, and at least one real place to go next. Three status codes, ONE shape: icon, title, description, actions — .bo-state and .bo-state--error, already shipped, nothing new. Not for a section failing to load inside a page you're still on — a table or panel that can't fetch its own data is .bo-state--error used inline, right where the section was, with the rest of the screen still usable. This pattern is for when the NAVIGATION itself failed, not one region of a page that otherwise worked.

This page doesn't exist

PO-99999 was not found — check the link, or it may have been moved.

Three codes, one shape

Same structure every time — only the icon, the sentence, and which actions make sense change. A reader who has seen one of these has seen all three.

You don't have access to this page

Cost-centre approvals for CC-9001 aren't in your role. Ask your manager to add it, or open a different cost centre.

Something went wrong on our end

The page failed to load. Nothing here is your fault — try again, or come back shortly.

The description always names the specific thing — "PO-99999", "CC-9001" — never a bare "Not found" or "Access denied". Same bar the field-error and validation- summary patterns already hold: name the datum, not just the category. The action set is the one real difference between codes: 404 and 403 offer navigation, never a retry — the resource genuinely isn't there or genuinely isn't yours, and a retry button that can't possibly help is a false promise. 500 is the one case retry can actually fix, so it's the primary action there instead.

Markup

<div class="bo-state bo-state--error">
  <span class="bo-state__icon" aria-hidden="true">🔍</span>
  <p class="bo-state__title">This page doesn't exist</p>
  <p class="bo-state__description">PO-99999 was not found — check the
    link, or it may have been moved.</p>
  <div class="bo-state__actions">
    <a class="bo-btn" href="/">Go to dashboard</a>
    <a class="bo-btn bo-btn--secondary" href="/pos">Search purchase orders</a>
  </div>
</div>

Anatomy

  1. Canvas, no persistent chrome — like login, this page does not assume the rest of the app is reachable: a 403 in particular may mean the sidebar's own links would also fail. Centered on --bo-color-bg-canvas, same as any screen outside normal navigation.
  2. The state block — .bo-state bo-state--error, unchanged from the fragment-level primitive. Full navigation and inline-swap failure share one component on purpose — see States below.
  3. Icon — aria-hidden="true"; the title and description carry the accessible signal, never the glyph alone.
  4. Title + description — what happened, in one sentence each, naming the specific record or resource where one exists.
  5. Actions — one or two buttons, chosen per code (see above); never zero. A dead end is the one failure mode this pattern exists to prevent.

Data contract

RequestReturns
GET /pos/:id (missing)a real 404, full HTML page — never a 200 with "not found" text; a search engine, a monitoring probe and a browser's own error handling all key off the status line, not the copy
GET /pos/:id (no access)a real 403, same page shape, the permission-specific copy
any request, server faulta real 500; the copy never guesses at a cause the server doesn't actually know
an htmx fragment request that 404s/403s/500sthe SAME .bo-state--error markup, sized for the swap target rather than the whole viewport — the shape doesn't change, only its container does. htmx 4 swaps non-2xx responses by default; exclude a status explicitly via htmx.config.noSwap if the swap target should NOT show it

States

StateWhat ships
Full navigation (404/403/500 at the URL)this page's own shape, canvas-centered, real HTTP status
Inside a fragment swapthe identical .bo-state--error markup sized to the swap target — a table that fails to load shows this INSIDE itself, the rest of the screen stays usable
500, retryableRetry is the primary action; a repeat failure shows the same page again rather than a different, more alarming one — consistency over escalation
404 / 403, not retryableno Retry button at all — offering one for a resource that is genuinely gone or genuinely not yours is a false promise, not politeness
Not authenticated, vs. authenticated-but-forbiddentwo different things a 403 must never conflate: a signed-out visitor gets redirected to login, never this page; THIS page is only for a signed-in user whose role genuinely lacks the permission
No JSfully functional — every element here is a plain link or the browser's own navigation; nothing on this page depends on JS running at all

Components used

complexity 1 of 4

↑↓ move Enter open Esc close