Skip to contentAsheeUIAsheeUI

EmptyState

The presentation of a region with nothing in it, why it is empty, and what fills it.

A region that shows nothing still has something to say. EmptyState is that sentence: a title that names the state in the reader's terms, a description that says what would fill it, and the one action that begins, set where the content would have been, so the absence explains itself in place rather than in a corner of the page.

What it is not is decoration for a first render. A state that is part of the page's first markup was not a change, so it announces nothing; a state that replaced content after a query is a result, and opts into status. role is that decision, and it is registered as none, a product whose empty states always answer a query states status once in configuration, and a state that was in the page's first markup is left alone.

Usage

import { EmptyState } from "@asheeui/web";

<EmptyState
  title="No invoices yet"
  description="Invoices you issue appear here, with their status and their total."
  primaryAction={{ label: "New invoice", href: "/invoices/new" }}
/>

title is the only required prop. primaryAction and secondaryAction are configuration rather than nodes: an action that navigates is written as { label, href }, and it renders through the same Button a hand-written action would, the primary action solid, the secondary bordered, both honouring the application's cascade.

An action that runs something is not a destination, so it goes in actions as a node. That is where a handler belongs:

<EmptyState
  title="No invoices yet"
  actions={
    <Button variant="solid" onClick={startInvoice}>
      New invoice
    </Button>
  }
/>

actions renders inside the same group as the configured pair, after it, so a state offering one destination and one handler still reads as a single row of choices. children is the slot below the actions, for anything else the state needs, the accepted import format, a sentence of help.

Why it is empty

type is the question the state answers, not a colour scheme. info is nothing here yet, which is expected; success is an emptiness the work produced; warning is an emptiness that wants attention; error is one something failed to fill. The tone colours the badge behind the icon, which is why a tone given no icon shows nothing at all, so pass an icon whenever the reason matters.

An empty result

A state that replaced content is a change, and a change is announced. Give it role="status" and a reader hears the sentence that explains the absence at the next pause, wherever their attention happens to be. role="alert" interrupts, so it belongs only to an emptiness that is itself a failure the reader has to hear now. Leave role at none, the registered value, for a state that was in the page's first markup: announcing a heading that was there from the start describes something that never happened.

The instance is not the only place the decision can be made. components.emptystate.role states it once for every empty state that does not say otherwise, which is what a product that renders its empty states in response to a search wants, and the instance still wins over it for the one state that is part of a first render.

The same rule is what DataTable applies to its own empty presentation, a status while it answers a query, silent on first paint.

Props

type, size, panel and role are the component's own tier: each resolves instance → components.emptystate → the framework's fallback, registered as info, md, false and none. None of them reads the application's defaultColor or defaultRadius, because the tone of an absence is not the product's accent, every empty state in an application says the same kind of thing in the same tone unless the product states otherwise, once, in configuration.

panel is the decision worth making deliberately. false lets the state sit directly on the page, which suits a region that is the page; true draws a surface, which suits a state standing in for content inside a dashboard, and is what a table uses for "no rows".

title, titleAs, description, icon, primaryAction, secondaryAction, actions, as and children are instance-only. What a particular absence means, where it sits in the page's outline, and which action ends it, are statements about one screen rather than about the design.

Accessibility

  • It is silent by default. No live-region role is rendered unless the instance, or components.emptystate.role, asks for one, so a state in the page's first markup is read as ordinary content when the reader arrives at it, and never as the announcement of something that just changed.
  • role="status" is the polite channel: after a filter or a query, the title and description are announced together at the next pause. role="alert" cuts in on whatever is being read, so it is reserved for an emptiness that is a failure in its own right.
  • The title is a heading, and titleAs states its level. It is an <h2> by default, so the state enters the document outline and a reader can reach it by heading navigation; titleAs states h1 when the state is the page and h4 when it sits inside a card's h3. Only heading elements are accepted, so the title keeps its place in the outline whatever it is set to.
  • The configured actions render as a real <button> or <a>, in the tab order after whatever precedes them, and actions children keep the semantics they were given. Nothing here moves focus: the state reports, and the reader decides whether to act.
  • The icon's badge is not hidden from assistive technology, so give a glyph or an image inside it aria-hidden, the title and description already say what the state is, and an unmarked glyph otherwise joins the announced sentence.

The two channels, and which surface announces what, are Announcements.