Skip to content

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.

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.

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

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.

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.

Exported symbols
ExportKindSignature
EmptyState7 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.

EmptyState props
PropTypeDefaultNotes
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.
titleReactNode—
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.

Luc UI v1.0EmptyStatedark theme