Empty state
StableOne heading that says what is missing, one line that says why, and one way forward, centred under an outlined icon tile.
Use it wherever a list, a search or a panel can come up empty. The heading names what isn’t there (“No runs yet”), the line explains or suggests, and the action, if any, is the single most useful next step.
Updated View source
Nothing here yet
import { Button, EmptyState, useToast } from '@rchursin/product/ui'
/** One heading that says what is missing, one line that says why, one way forward. */export default function EmptyStateBasic() { const toast = useToast() return ( <div className="w-full max-w-md rounded-2xl ring-1 ring-line ring-inset"> <EmptyState icon="run" title="No runs yet" action={ <Button size="sm" variant="secondary" icon="terminal" onClick={() => toast({ title: 'Sample: the setup steps would open here', description: 'Install the CLI, then run your agent once.', icon: 'terminal' })} > Show setup steps </Button> } > Runs show up here as soon as an agent sends its first trace. </EmptyState> </div> )}Usage
import { EmptyState } from '@rchursin/product/ui'<EmptyState icon="run" title="No runs yet" action={<Button size="sm" variant="secondary">Show setup steps</Button>}> Runs show up here as soon as an agent sends its first trace.</EmptyState>Examples
No search results
Say what was searched for and offer the way back. Clear search empties the field and puts focus in it; the count is a polite live region.
import { useRef, useState } from 'react'import { Button, EmptyState, Field, TextInput } from '@rchursin/product/ui'
const runs = ['Nightly eval', 'Docs agent sync', 'Release notes draft', 'Cost report rebuild']
/** * Nothing matched: say what was searched for and offer the way back. "Clear search" empties the * field and puts focus in it. The count above the list is a polite live region. */export default function EmptyStateSearch() { const [query, setQuery] = useState('deploy') const input = useRef<HTMLInputElement>(null) const q = query.trim().toLowerCase() const found = runs.filter((r) => r.toLowerCase().includes(q))
return ( <div className="flex w-full max-w-sm flex-col gap-3"> <Field label="Search sample runs">{(p) => <TextInput {...p} ref={input} type="search" icon="search" value={query} onChange={(e) => setQuery(e.target.value)} />}</Field> <p aria-live="polite" className="text-xs text-fg-3"> {found.length === 1 ? '1 run' : `${found.length} runs`}API
Empty state props
| Prop | Type | Default | Description |
|---|---|---|---|
icon | IconName | 'square-dashed' | Drawn in an outlined tile above the heading. Decorative. |
titlerequired | string | – | What is missing, as an h3, such as “No runs yet”. |
children | ReactNode | – | One line: why it is empty, or what to try. |
action | ReactNode | – | One button or link, the most useful next step. |
className | string | – | Classes on the wrapper (centred, at most 20 rem wide, 40 pixels of space above and below). |
Accessibility
- The heading is an h3, so place it under an h2 (a panel or section title).
- The icon tile is decorative; the heading and line carry the meaning.
- It is plain content, not a live region. When it appears because of a search, say the result count in a polite live region, as the search example does.
- If the action clears a filter or search, put focus back where the person will type next.
Guidelines
Do
- Say what is missing, not that something went wrong.
- Offer one action that fixes it or gets started.
- Repeat the search words in a no-results heading.
Don’t
- Don’t leave a blank panel with no words.
- Don’t show it while data is still loading; show a spinner until you know.
- Don’t add a second or third button; one way forward.
In use
Related
- SpinnerThe loader icon turning once every 900 ms, only when motion is allowed. Decorative next to words; give it a label when it stands alone.
- Task listThe task list as a data grid: one Tab stop, arrows to move, pickers in the cells, and groups that collapse.
- Text inputA one-line field in the inset lux-input finish, in two sizes, with an optional icon inside and a slot on the right.
- Time logWhat was focused on, by day with daily totals and a seven-day chart, with time added by hand and Undo on delete.
Source
The component’s own files, exactly as they run on this site.
/** * Small pieces: avatars, chips, tooltip, empty state. */import { cloneElement, isValidElement, useEffect, useId, useRef, useState, type ReactElement, type ReactNode,} from "react"import { cx } from "../../components/cx"import { Icon, type IconName } from "../icons/Icon"import { computePosition } from "./popover"
/* ---------- Avatar: initials on a grey square (the brand square, softened) ---------- */
const tones = [