Empty State
The “nothing here yet” panel — before the first record exists, after a filter matches nothing, or when a feature has not been used.
No projects yet
Create your first project to start deploying. It takes about a minute.
Usage#
import { Button, EmptyState } from "neelam-ui";
<EmptyState
icon={<FolderPlus className="h-5 w-5" />}
title="No projects yet"
description="Create your first project to start deploying."
action={<Button>New project</Button>}
/>Write the title as a short statement of what is missing — "No projects yet",
not "Empty". The description is what turns a dead end into an instruction, and
action is the way out.
Choosing the heading level#
title renders as a <p> by default, not a heading. An empty state can
appear inside a card, a table body, a tab panel, or as a whole page, and only
you know which level the surrounding outline calls for — guessing produces
skipped heading levels (WCAG 1.3.1).
Pass titleAs where the empty state owns a section:
<EmptyState titleAs="h2" title="No results" />Why titleAs names the element
The alternative — passing a heading element as title — would nest an
<h2> inside the <p> that styles it, which is invalid HTML. Naming the
element lets the level be chosen without that.
This is the same problem Card has, resolved
differently: a card's nesting is predictable enough to fix CardTitle at
<h3>, and an empty state's is not.
Announcing a state that replaces content#
live renders the region as role="status" with aria-live="polite", so an
empty state that appears in place of content is announced — a filter matching
nothing, or a finished load returning zero rows:
<EmptyState live title="No results" description="Try a different search." />Leave it off — the default — when the empty state is there on first paint. A live region announces changes, so one present from the start announces nothing anyway, and marking it live only risks a stray announcement later.
Keyboard#
| Key | Behaviour |
|---|---|
| Tab | Reaches whatever you pass as action. The panel itself is not focusable. |
Move focus when the state swaps in
If an empty state replaces a list the user was working in, their focus may
have just been destroyed with it. Moving focus to the empty state's action —
or its heading, with tabIndex={-1} — keeps the keyboard user oriented.
Accessibility#
iconisaria-hiddenthroughout. It always duplicates the title beside it, the same treatmentBadgeandButtongive their icon slots.- The default
<p>title avoids inventing a heading level, which is the more common mistake than omitting one. liveis opt-in rather than always on, so an empty state on first paint does not register a live region that can only misfire later.- The dashed border is decoration; the title and description carry the meaning, so the state reads correctly with styles off.
API reference#
| Prop | Type | Default |
|---|---|---|
titlerequiredWhat's missing, as a short statement — "No projects yet", not "Empty". | ReactNode | — |
actionThe way out — typically a `Button`. Rendered under the description. | ReactNode | — |
descriptionWhy it's empty and what will fill it. Optional, but it's what turns a dead end into an instruction. | ReactNode | — |
iconA decorative illustration or icon above the title. Hidden from assistive tech. | ReactNode | — |
liveRenders the region as `role="status"` with `aria-live="polite"`, so an empty state that *replaces* content in place (a filter matching nothing, a finished load returning zero rows) is announced. Leave off — the default — when the empty state is present on first paint, where a live region would announce nothing anyway. | boolean | false |
titleAsWhich element renders the `title`. Defaults to `"p"` — pass the heading level the surrounding outline calls for (`"h2"`, `"h3"`, …) where the empty state owns a section of the page. | enum | p |
Also accepts every native <div> attribute except title, which is taken over
above. Anything passed as children renders after the action.