Skip to content

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.

Built fromEmptyStateButtonCard

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.

Dooffer 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'tstate 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.

Dooffer to clear the filters

When filters caused the emptiness, undoing them is the action people actually want.

Inbox zero

No deployments need review.

Don'tpush an action after a success

Finishing everything is a good outcome. Do not immediately ask for more work.

Code examplePermalink to this section

app/projects/projects-list.tsx
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.