Feedback
EmptyState
The reusable “nothing here yet” surface, so a page, a card, a pane and a table region never invent icon plus heading plus paragraph plus button a fourth time.
import { EmptyState } from "@luc/ui";Examples
Page level
Calm by default: one mark, one line of what is missing, one sentence of what to do, and the action that does it.
No research runs yet
Artifacts are written to luc-data/research. Start a study to see the registry fill up.
<EmptyState icon={<RiFolderOpenLine />} title="No research runs yet" description="Run a study to see its artifacts here." action={<Button>New run</Button>} secondaryAction={<Button variant="ghost">Browse templates</Button>} />Inside a card
The same component, sized by its own padding. Nothing about it assumes a full-page canvas.
No chart selected
Pick an instrument to load its prepared series.
No backtest artifacts
Reports appear here once a run finishes.
Nothing pinned
Pin a timestamp to compare across charts.
<Card className="p-0">
<EmptyState
size="sm"
icon={<RiLineChartLine />}
title="No chart selected"
description="Pick an instrument to load its prepared series."
action={<Button size="sm">Add instrument</Button>}
/>
</Card>No search results
The most common empty state of all. The difference between “empty” and “filtered to nothing” is worth a distinct message and a reset — never the same panel with different words.
- btc-sma-2026-09
- eth-momentum-2026-09
- macro-regime-2026-08
{matches.length > 0 ? (
<ul>{matches.map((run) => <li key={run}>{run}</li>)}</ul>
) : (
<EmptyState
size="sm"
icon={<RiFilterOffLine />}
title="No runs match that filter"
description={`Nothing matches "${query}".`}
action={<Button size="sm" variant="secondary" onClick={clear}>Clear filter</Button>}
/>
)}Sizing and semantics
Small is for a table region or a pane; medium is for a page or a card. The title is a paragraph unless you say otherwise, and nothing is announced as a live region.
Small
Compact padding, 32px mark.
Medium
Page and card rhythm, 40px mark.
Announced when it appears
Pass a role when emptiness is an event rather than a state.
<EmptyState size="sm" title="Small" description="Compact padding, 32px mark." />
<EmptyState size="md" title="Medium" description="Page and card rhythm, 40px mark." />
/* Emptiness as an event, not a state. */
<EmptyState size="sm" role="status" title="Announced when it appears" />Usage
import { EmptyState } from "@luc/ui";<EmptyState
title="No runs yet"
description="Start one from the toolbar and it will appear here."
action={<Button>New run</Button>}
/>Static by design: pass `role="status"` yourself when the emptiness happens after the page has loaded.
API
Read from the published TypeScript API, so a prop that is not listed here does not exist and a default that is listed is the one the compiler sees.
One component, two sizes, and a `titleAs` that decides whether its title is a heading or a paragraph.
| Export | Kind | Signature |
|---|---|---|
EmptyState | 7 documented props | ({ className, icon, title, description, action, secondaryAction, size, titleAs: TitleTag, children, ...props }: EmptyStateProps) => import("@types/react").JSX.Element |
EmptyState props
Type: EmptyStateProps
Values the compiler reads from the declaration.
| Prop | Type | Default | Notes |
|---|---|---|---|
action? | ReactNode | — | Primary action — usually a Button. |
description? | ReactNode | — | |
icon? | ReactNode | — | Optional mark. Sized and coloured by the component; pass a bare icon. |
secondaryAction? | ReactNode | — | Secondary action, rendered beside the primary one. |
size? | "sm" | "md" | undefined | — | `sm` for a table region or a pane; `md` for a page or a card. |
title | ReactNode | — | |
titleAs? | "h2" | "h3" | "p" | "h4" | undefined | — | Element used for the title. Defaults to a paragraph so the component can be dropped anywhere without inventing a heading level. |
Guidelines
Do
- Answer two questions: why is this empty, and what fills it. An empty state with no action is a dead end.
- Size it to its container — `sm` in a table region or a pane, `md` for a page or a card.
- Choose the mark from the reason: an empty folder, a search that found nothing, a lock for a permission the reader lacks.
Don't
- Do not use it for a failure or a wait; an error is a `Result`, and a wait is a spinner or a skeleton.
- Do not write "No data" and stop: the reader cannot tell whether to wait, retry or change a filter.
- Do not put the page's own heading inside it — the component renders a paragraph unless `titleAs` says otherwise.
Accessibility
The component is static content with no implicit role, and its title is a paragraph by default so it cannot invent a heading level inside a layout it does not control — pass `titleAs` when the state really is the region's heading. Nothing is announced when it renders on the server, which is right for a page that was empty when the reader arrived; when emptiness follows an action, `role="status"` is what makes the change audible.
Related