Skip to content

EmptyState

Terminal window
$ npx shadcn@latest add https://lacodda.github.io/dowel/r/empty-state.json
See it liveNothing yet, nothing matching, and nothing loaded.→

Three kinds of nothing, and a product that draws the same panel for all three tells the reader the wrong thing twice:

What happened The way out
empty Nothing here yet, and that is normal Create the first one
filtered Plenty here, none of it matching Widen the filter
error It could not be fetched Try again

They differ in what the reader should do next — which is what an empty screen is for — so they are a variant rather than three components.

<EmptyState
title="No works yet"
body="Everything you start will show up here."
action={<Button variant="primary">New work</Button>}
/>

action is the point. An empty screen with no way out is a dead end. It is optional in the type because a small panel can borrow the way out from its surroundings — but a full-page empty state without one is a bug the reader cannot report.

The error variant is not dashed. A dashed border reads as a placeholder for something that belongs there, and a failure is not that; it takes a solid, tinted panel and says the title in the failure colour.

The mark is a watermark, not a logo. It is the line’s hexagon in the current text colour rather than a product’s accent — a full-strength mark in an empty panel shouts. It is inlined so it costs no request and can take the theme’s colour, and it is aria-hidden, because it says nothing a reader needs. Pass mark to put a product’s own illustration there instead.

plain is the same three kinds with no frame. The framed panel is for a screen or a region that is empty. Inside something that already has a frame — a widget, a card, a panel’s list — a dashed box and a watermark are a frame inside a frame, and products drew those empties as bare text instead, without the way out. Plain keeps the words and the action and drops the rest: no border, no mark, set in the flow on the left, where the content would have started.

<Panel>
<SectionLabel>Score</SectionLabel>
<EmptyState plain title="Not scored yet." action={<Button size="sm">Score it</Button>} />
</Panel>

Where it is used automatically: QueryState draws it for a failure, and ErrorBoundary for a crash.

The marks are exported. EmptyMark, FilteredMark and ErrorMark are there for a product composing its own panel.

Props

Read from the types in ui/empty-state.tsx, the file a product copies - so the description of each prop is also what an editor shows over it there.

EmptyMark

The line's hexagon, at whatever size the caller asks for.

PropTypeDefault
classNamestring

FilteredMark

The same hexagon, cut by a slash. For a filter that matched nothing: the shape is there, the contents are not.

PropTypeDefault
classNamestring

ErrorMark

A hexagon with a corner broken out of it. For something that failed.

PropTypeDefault
classNamestring

EmptyState

PropTypeDefault
variantempty | filtered | error'empty'
plainbooleanfalse
titlerequiredReactNodeWhat is not here, in the product's words.
bodyReactNodeWhy, or what to do about it.
actionReactNodeThe one thing worth doing here. An empty screen with no way out is a dead end.
markReactNodeSomething other than the default mark - a product's own illustration. A plain empty state draws no mark, its own or the default.

And every prop of React's HTMLAttributes<HTMLDivElement> except title, passed on.

Accessibility

The examples on the stand, through axe 4.14.0 in a browser, in both themes, against WCAG 2.2 A and AA and axe's best practices: 7 rules applied and passed, and none failed. The accessibility report has every item.

History

Each release that changed ui/empty-state.tsx - the file a product copies - newest first.

  1. v0.33.0
    • feat plain empty states and a retry for a failed load ce765d4
  2. v0.23.0
    • feat the screens between asking for data and showing it bc3f636