Pattern
Empty state
What a screen says when it has nothing to show. There are four different reasons a screen can be empty, and each needs a different answer — treating them all the same is the most common mistake in this pattern.
StructurePermalink to this section
An icon, a heading, one sentence of explanation, and a way forward. The action is what turns an empty screen into a starting point.
No projects yet
Projects hold your pages, deployments and team access. Your first one takes about a minute to set up.
Four kinds of emptyPermalink to this section
Read the heading of each and notice how differently they behave. Only the first one is an achievement waiting to happen.
No projects yet
Create your first project to get started.
First run
Nothing has been created yet
No projects match “billing”
Try a different search, or clear the region filter.
No results
A filter excluded everything
Inbox zero
No deployments need your review right now.
Cleared
Everything has been dealt with
You do not have access
Ask a workspace admin to grant you the Developer role.
Blocked
Permission or connectivity
- First run — the screen is empty because nothing exists yet. Explain what the thing is for and offer to create one.
- No results — the data exists but the filters excluded it. Echo the query, and offer to widen or clear it.
- Cleared — the person finished everything. Confirm it plainly and do not push another action.
- Blocked — permission, connectivity or an error. Say what is wrong and who or what can resolve it.
Errors are not empty statesPermalink to this section
A failed request is a different message with a different action — retrying, not creating.
Could not load projects
The request timed out. Your projects are safe — this is a problem reaching the server.
Never say “no projects” when the request failed
Reporting an error as emptiness makes people think their data is gone. Distinguish the two states in the data layer before the component is chosen.
Content guidelinesPermalink to this section
- The heading states the situation, not the emotion: “No projects yet”, not “Nothing here!”.
- The description explains what the thing is for, so a first-time reader learns something.
- One primary action, matching the label of the button that would normally create the item.
- Echo the query in a no-results heading — “No projects match “billing”” confirms what was searched.
- Never blame the person: “No projects match this filter”, not “You have not created any projects”.
- Skip the action entirely when the empty state is a success, such as an empty inbox.
- Keep the icon abstract and structural. No illustrations, no mascots.
Do and don'tPermalink to this section
No projects yet
Create your first project to get started.
Do — offer the next step
An empty screen with an action is a starting point rather than a dead end.
No data
There is no data.
Don't — state the obvious and stop
“No data” tells someone what they can already see and gives them nowhere to go.
No projects match “billing”
Try a different search, or clear the region filter.
Do — offer to clear the filters
When filters caused the emptiness, undoing them is the action people actually want.
Inbox zero
No deployments need review.
Don't — push an action after a success
Finishing everything is a good outcome. Do not immediately ask for more work.
Code examplePermalink to this section
import { Button, EmptyState } from "@borneo/react";
import { FolderPlus, SearchX, WifiOff } from "lucide-react";
export function ProjectsList({ projects, query, error, onRetry, onClear }) {
// Failure is checked first — an error is never reported as emptiness.
if (error) {
return (
<EmptyState
icon={<WifiOff />}
title="Could not load projects"
description="The request timed out. Your projects are safe."
action={
<Button variant="secondary" onClick={onRetry}>
Try again
</Button>
}
/>
);
}
if (projects.length === 0 && query) {
return (
<EmptyState
icon={<SearchX />}
title={`No projects match “${query}”`}
description="Try a different search, or clear the filters."
action={
<Button variant="secondary" onClick={onClear}>
Clear filters
</Button>
}
/>
);
}
if (projects.length === 0) {
return (
<EmptyState
icon={<FolderPlus />}
title="No projects yet"
description="Projects hold your pages, deployments and team access."
action={<Button>Create project</Button>}
/>
);
}
return <ProjectGrid projects={projects} />;
}Responsive behaviorPermalink to this section
The empty state centres in whatever container holds it. On mobile the actions stack, with the primary on top.
No projects yet
Projects hold your pages, deployments and team access.
AccessibilityPermalink to this section
An empty state replaces content, so it has to be announced when it appears.
- The heading renders as a real h3, keeping the page's heading order intact.
- The icon is aria-hidden — the heading already says what the icon depicts.
- When the state appears after a filter change, wrap the region in aria-live="polite" so the result is announced.
- Actions are real buttons, reachable in the normal tab order.
- The description clears 4.5:1 at 13px against both the canvas and the muted surface.
- Do not trap focus or move it automatically — the person is still working in the filter controls above.