Patterns / ErrorView
ErrorView
The error view tells a reader that something has failed and what that means for them. It says whether trying again is likely to help and offers the retry where it is, so a failure is something the reader can respond to rather than only observe.
Like ConfirmationModal, this one the package ships: ErrorView is a module — components arranged in one fixed way and published as a single export, imported exactly the way Button is.
Inside it is an EmptyState fixed to its placeholder type, a glyph carrying the accent, an optional technical line, and a row holding the retry Button plus whatever else you pass. Nothing you could not have assembled — the point is that every failure in every product is now the same one, down to whether a retry is offered at all.
Error while loading portfolios
Something isn't right. This is usually temporary, and the rest of the page is fine.
request 4f2c91b0 · attempt 1
The retry is real — press it and the detail line counts up, which is the whole of what onRetry does: it draws the button and calls your handler. It does not close anything, clear anything or reload the page, because a failure that has not cleared should stay on screen.
Error while loading news
The section failed. Trying again is worth a shot.
Trend data is too sparse
There is not enough signal here to produce a summary. Try a more general search term.
Both tones, and the difference that matters is on the right: no onRetry, so no button. The data will be exactly as sparse next time, and a retry that cannot succeed teaches people to press it three times and leave. Passing fill={false} is what lets these two sit side by side — on a real surface each would own its region.
When to use
Reach for an error view wherever a request did not come back and a region is left with nothing it can fill itself with — a panel, a table, a whole page. Name the part that failed, say whether the reader should expect it to clear, and offer the retry only where trying again could change the answer. The habit that most often goes wrong is a box that apologises without saying what is missing, which leaves a reader on a page of several sections working out which one to reload.
When not to use
Do not reach for one where nothing failed. An absence that was expected — an empty folder, a finished job, a feature still to come — belongs to EmptyState, and a danger-coloured glyph on it reads as a bug that is not there. An answer still on its way is Skeleton or Spinner rather than a failure declared early. And a single control that will not accept what was typed is not a failed region: that message belongs beside the control, where the reader can act on it without losing what they wrote.
tone
The line between the tones is whether a retry is worth the reader’s time. It is deliberately not a list of HTTP statuses: a 404, a 500 and a dropped connection produce the same screen, because what the reader can do about each is identical. What genuinely differs is whether doing it again would change anything — and that is a question about the failure, not about its code.
| tone | When to use |
|---|---|
error | Something broke, and trying again may well work. --status-danger on the glyph, and the tone to pass when a request failed, a save did not land, or a section came back empty when it should not have. |
unavailable | The result cannot be produced right now, and a retry returns the same nothing — the data is too sparse to answer, the upstream service is down, the feature is switched off. --status-warning on the glyph. Usually paired with no onRetry at all, because offering one is a promise the next attempt will break. |
Retry offered
onRetry is what draws the button: a ghost Button under the copy, labelled “Try again” unless retryLabel names something narrower. Pressing it calls your handler and does nothing else — the box does not close, clear or reload, because a failure that has not cleared should stay on screen.
With the technical line
detail adds a small line below the description, in --text-additional rather than --text-disabled so it survives being read out loud, and it breaks anywhere so a request id cannot push the box wide. Pass one where somebody will be asked to quote it, and leave it off otherwise.
Inside a surface that already exists
filled and fill both default on here, which is the shape a failure that owns its region wants, and each is turned down separately. Drop filled inside a card that already draws a surface, so the box does not tint a tint; drop fill where there is no leftover height to grow into — two views side by side, or a box under copy that has to stay its own size. Left on, fill still needs a flex-column parent to grow against.
Name what failed in the title. “Error while loading portfolios” tells a reader which part of the page is missing and what still works; “Something went wrong” makes them guess, and on a page of several sections it does not even say which one. The tone’s glyph is hidden from assistive technology, so neither the drawing nor its colour reaches a reader who is listening — the title and the copy under it carry the whole message.
This is a region of copy, not an alert: it adds no role of its own, so nothing about it interrupts a reader mid-task. What is read is the title — a real heading, and EmptyState’s titleAs is still what fits it to the outline of the page around the box — then the description, the detail line and the labels below them.
No onRetry is a decision, not an omission. A retry button on a failure that cannot clear teaches people to press it three times and then leave. If trying again would return the same nothing, pass tone="unavailable" and no handler.
unavailable is not EmptyState’s placeholder. Placeholder means not built yet — a stage or a tab that is coming. Unavailable means built, and unable to answer right now. The first is a roadmap and the second is a failure; a reader who confuses them waits for the wrong thing.
Resolve the error before it gets here. This takes a title and a tone, not an Error — deciding that one message is safe to show and another is a status code to hide is your application’s knowledge, and a design system that guessed at it would be wrong in a different way for every product.
Keep detail quotable and short. A status and a request id are what a reader relays to support; a stack trace is what they screenshot instead of reading, and it puts your internals on their screen.
There is no reload prop. A full page reload is a decision about the application, not about this box — put a Button in children where you want one, and let the retry mean the narrower thing.
A mutation that failed usually wants no retry here at all. The reader’s input is still in the form behind it, so a Cancel that closes and lets them press Save again beats a retry that re-fires a request they cannot see.
A failure is one shape, which is why type, droppable and register do not reach this module: an error view is always the bordered box, nothing about a failure accepts a drop, and the tone already owns its colour — an expressive failure would be the brand celebrating one. Reach for EmptyState directly if you want any of the three, and accept that what you are drawing is then not a failure.
The root is EmptyState’s, so a rule written for every [data-slot="empty-state"] on the page hits every failure too. Where the two should differ, select on [data-module="error-view"] beside it — the module adds that attribute rather than replacing the slot, precisely so a consumer’s existing rules keep working.
A second way out
children land in the same row as the retry, after it: Cancel, Reload, Back to dashboard. They are real Buttons and you pass them, which is why there is no actions array — onRetry plus children already reaches every arrangement. Match the retry’s ghost emphasis while you are there, or the escape hatch ends up louder than the thing you want pressed.
A glyph of your own
icon replaces the drawing the tone chose, and the accent goes with it — the colour lives on a span the module wraps its own glyph in, so yours arrives in the surrounding text colour. Worth it where a product has a truer mark for what failed, a lock or a clock or one integration’s own, and worth skipping otherwise: the tone’s pair of drawings is what keeps “broken” and “cannot answer” apart at a glance.
Everything EmptyState takes, less type, droppable and register — a failure is one shape, nothing about it accepts a drop, and tone already owns its colour, so an expressive failure would be the brand celebrating one. filled and fill are defaulted the other way round, because a failure that owns its region has to read as a surface. The table below is the module's own props plus the inherited ones an error view always sets.
Three things it deliberately does not take. There is no Error prop: deciding which backend message is safe to show a reader and which is a status code to hide is your application's knowledge, and a shared package guessing at it would be wrong differently for every product. There is no actions array, because onRetry plus children already reaches every arrangement. And there is no reload, since a full page reload is a decision about the application rather than about this box — put a Button in children where you want one.
| Prop | Type | Default | Notes |
|---|---|---|---|
tone | "error" | "unavailable" | "error" | Picks the glyph and the accent together, so a warning-coloured mark can never end up on the tone that means something broke. It reaches the DOM as data-tone. |
title | string | — | Required, and it displaces the div’s own title attribute — this is copy the reader sees, not a tooltip. Name what failed rather than apologising. |
description | ReactNode | — | Whether this is likely to clear, and what the reader can do meanwhile. It is held to a readable measure, so a sentence or two rather than a paragraph. |
detail | ReactNode | — | The technical line — a status, a request id, an error type. Opt-in, and for the cases where someone will be asked to quote it. Never a stack trace. |
onRetry | () => void | — | Present draws the retry button; absent draws none. Leave it off when the same attempt would fail the same way — that absence is the honest signal. |
retryLabel | string | "Try again" | The retry button’s label. “Reload”, “Retry section”, “Start over”. |
icon | ReactNode | the tone’s glyph | Overrides the glyph the tone chose. Passing one also drops the accent, since the colour travels with the drawing. |
filled | boolean | true | EmptyState’s prop, defaulted the other way: a failure that owns its region needs to read as a surface. |
fill | boolean | true | Also flipped — a failure view is the whole content of its region. Its parent still has to be a flex column. |
children | ReactNode | — | Further actions beside the retry, in the same row: Cancel, Reload, Back to dashboard. They are real Buttons and you pass them. |
error-view-detail | [data-slot] | — | The paragraph holding the detail line. It is the handle for a monospace face on a request id, which the module does not impose. |
error-view-actions | [data-slot] | — | The row holding the retry and whatever came in children. It wraps and centres — change the gap or the alignment here rather than wrapping the buttons in something of your own. |