Empty state
What a list or page shows when there is nothing in it: why it is empty and the one next step. First run, no match, not connected yet, or failed to load.
Playground
Change the controls to try each prop. The code under the preview updates as you go and lists only the props you changed; copy it into your page. The examples below show the full layout with its parts.
import { Empty } from '@/ui/empty';<Empty> No notes on Halcyon Bank yet.</Empty>First run
Nothing exists yet. A 16px icon on the 32px tile, the title, one line, then one primary and an optional ghost action.
No companies yet
Drop a spreadsheet anywhere on this page, or add your first client by hand.
import { BuildingsIcon } from '@phosphor-icons/react';
import { Button } from '@/ui/button';
import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from '@/ui/empty';
export default function EmptyDefault() {
return (
<Empty>
<EmptyHeader>
<EmptyMedia variant="icon">
<BuildingsIcon />
</EmptyMedia>
<EmptyTitle>No companies yet</EmptyTitle>
<EmptyDescription>Drop a spreadsheet anywhere on this page, or add your first client by hand.</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<Button variant="primary">Import</Button>
<Button variant="ghost">Add by hand</Button>
</EmptyContent>
</Empty>
);
}No matches
Records exist but the search or filters hide them. No tile: say how many are hidden and offer the way back.
No candidates match this view
Try a different search, or clear the filters to see every candidate.
import { Button } from '@/ui/button';
import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyTitle } from '@/ui/empty';
export default function EmptyNoMatch() {
return (
<Empty>
<EmptyHeader>
<EmptyTitle>No candidates match this view</EmptyTitle>
<EmptyDescription>Try a different search, or clear the filters to see every candidate.</EmptyDescription>
</EmptyHeader>
<EmptyContent>
12 hidden by filters
<Button>Clear search and filters</Button>
</EmptyContent>
</Empty>
);
}Not connected yet
The page needs a connection first. The button names what it connects.
Connect your mailbox
Inbox shows mail with your clients and candidates once your mailbox is connected.
import { EnvelopeSimpleIcon } from '@phosphor-icons/react';
import { Button } from '@/ui/button';
import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from '@/ui/empty';
export default function EmptyNotConnected() {
return (
<Empty>
<EmptyHeader>
<EmptyMedia variant="icon">
<EnvelopeSimpleIcon />
</EmptyMedia>
<EmptyTitle>Connect your mailbox</EmptyTitle>
<EmptyDescription>Inbox shows mail with your clients and candidates once your mailbox is connected.</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<Button variant="primary">Connect mailbox</Button>
</EmptyContent>
</Empty>
);
}Inside a section
size="inline" is one muted line, with no tile and no button of its own. The section's own Add action does the work.
Notes
import { PlusIcon } from '@phosphor-icons/react';
import { Button } from '@/ui/button';
import { Empty } from '@/ui/empty';
export default function EmptyInline() {
return (
<section aria-labelledby="notes-heading" className="flex w-full max-w-[628px] flex-col gap-100 rounded-xlarge border border-border bg-surface px-[20px] py-200">
<div className="flex items-center justify-between">
<h3 id="notes-heading" className="text-heading-small leading-body font-semibold tracking-heading-small text-text">
Notes
</h3>
<Button variant="ghost">
<PlusIcon />
Add note
</Button>
</div>
<Empty size="inline">No notes on Halcyon Bank yet.</Empty>
</section>
);
}Failed to load
The same layout with the danger tile and a secondary Try again. Never blame the person.
This page didn't load
Check your connection, then try again.
import { WarningCircleIcon } from '@phosphor-icons/react';
import { Button } from '@/ui/button';
import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from '@/ui/empty';
export default function EmptyError() {
return (
<Empty>
<EmptyHeader>
<EmptyMedia variant="icon" tone="danger">
<WarningCircleIcon />
</EmptyMedia>
<EmptyTitle>This page didn't load</EmptyTitle>
<EmptyDescription>Check your connection, then try again.</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<Button>Try again</Button>
</EmptyContent>
</Empty>
);
}Heading level
The title is an h2. Pass render={<h3 />} when the page's outline needs another level. Links in the description are brand green and underlined.
No meetings this week
Meetings show here once your calendar is connected. Connect your calendar
import { Empty, EmptyDescription, EmptyHeader, EmptyTitle } from '@/ui/empty';
export default function EmptyHeadingLevel() {
return (
<Empty>
<EmptyHeader>
<EmptyTitle render={<h3 />}>No meetings this week</EmptyTitle>
<EmptyDescription>
Meetings show here once your calendar is connected. <a href="#connect-calendar">Connect your calendar</a>
</EmptyDescription>
</EmptyHeader>
</Empty>
);
}Duplicate notice
A row that appears under a Company you just saved when Ascern finds one that looks the same. It points it out and never blocks the save.
Environment badge
Marks a sandbox Agency on every screen, so nobody mistakes practice data for real candidates. It sits in the sidebar under the Agency name.