Skip to content

Empty States

The most expensive screen in a product is the one with nothing on it and no way forward. Four different kinds of empty, four different jobs.

Also called Zero State, Blank Slate, No Data — in this system all of them are Empty States.

Live preview

Everything below is the real component. Change the controls, tab through it, and turn on Inspector Mode to read any value off the screen.

Playground

No projects yet

A project holds your services, environments and deployments. Most teams start with one per repository.

The four kinds

Same component, four different jobs. Note how the action changes: create, widen, review, escalate.

No projects yet

A project holds your services, environments and deployments. Most teams start with one per repository.

No deployments match “gateway-v3”

Try a shorter search term, or clear the environment filter to widen the results.

Inbox zero

Everything is handled. New incidents will appear here as they are opened.

You do not have access to this project

Ask an administrator to add you, or switch to a workspace you are a member of.

Inline and compact

An empty table body, an empty sidebar section, an empty card. The compact variant halves the vertical padding so it does not dwarf its container.

Deployments

No results

No deployments match the current filters.

Recent activity

Nothing in the last 7 days.

First run as onboarding

The most valuable empty state in any product. It gets read once, by every new user, at the exact moment they are deciding whether to continue.

Deploy your first service

Connect a repository and we will build and deploy it on every push. Most teams are live in under four minutes.

Read the two-minute quickstart

Interaction states

Every state a user can put this component into, rendered side by side. If a state is missing here, it is missing in production too.

No projects yet

First run

No deployments match “

No results

Inbox zero

Cleared

You do not have access

No access

Could not load deploym

Error

Nothing here

Compact

Inbox zero

Everything handled.

No icon

Empty

No actionA dead end

Anatomy

Every part, every measurement, and the reason it is that number.

No projects yet

A project holds your services, environments and deployments.

Icon, title, description, primary action, secondary action. Everything is centred, and the whole block is capped at 24rem so it never spans a wide container.

  1. Icon container56px, 16px radius

    A tinted square, not a large illustration. Big enough to anchor the block, small enough to keep the action above the fold.

  2. Vertical padding64px, 40px compact

    Generous, because the emptiness is the point — a cramped empty state reads as a rendering failure rather than a designed state.

  3. Title19px · text-h3

    A real heading element, so screen-reader users landing in the region understand it immediately.

  4. Description widthmax 24rem

    About 45 characters per line. Centred text needs a narrower measure than left-aligned, because every line starts in a different place.

  5. Action gap16px below the text

    Close enough to read as the answer to the description. A large gap makes the action look like a separate, optional thing.

  6. Secondary actionText variant, beside

    One primary and at most one secondary. A third action turns guidance into a menu.

Design tokens used

Values are read live from the running stylesheet, so this table can never drift from the code. Click any value to copy it.

Color

TokenValueUsed for
--ds-surface-inset—Icon container fill
--ds-border-subtle—Icon container border
--ds-fg-muted—Icon and description
--ds-accent-subtle—Icon fill on an onboarding state
--ds-danger-subtle—Icon fill on an error state

Spacing

TokenValueUsed for
padding-yVertical breathing room
gapBetween icon, text and actions

Radius

TokenValueUsed for
--radius-xlIcon container

Typography

TokenValueUsed for
--text-h3—Title
--text-body-sm—Description

Recommended sizes

Pick a size from this table. Do not invent a new one — a fourth height is how a design system starts dying.

SizePaddingIconLabel gapMax widthWhen to use
Compact40px 24px44px container12px24remInside a table body, a card, or a sidebar section.
Default64px 32px56px container16px24remA full page region — the main content area of a screen.
Onboarding56px 32px56px container20px28remFirst run. Room for two actions and a documentation link.
Inline12px 0———One line of muted text. No icon, no action, for genuinely unremarkable emptiness.

Do

No projects yet

Always give one clear next actionAn empty state without an action is a dead end. The user came here to do something; tell them what that something is.
No data → Create a projectNo results → Clear filters
Distinguish "no results" from "no data"They need opposite actions. No results should widen the filter; no data should create the first item. Showing "Create a project" when a search failed is actively unhelpful.

No deployments match “gateway-v3”

Try a shorter term, or clear the environment filter.

Echo the search term back"No deployments match gateway-v3" confirms what was searched. "No results" leaves the user wondering whether their query even registered.

Deploy your first service

Connect a repository and we build and deploy on every push.

Treat first run as onboardingIt is read once, by every new user, at the moment they decide whether to continue. Explain the concept, not just the button — this is the highest-leverage copy in the product.

Don't

No data
Do not leave a dead endA grey box saying "No data" gives the user nothing. They cannot tell whether something is broken, whether they lack permission, or what they should do.
…action is somewhere below
Do not use a huge illustrationA 400px graphic pushes the action below the fold on a laptop and makes an already-empty region feel emptier. The action is the point.

Oops! Nothing to see here 🙈

…the user has been denied access to their own project

Do not be cute at the wrong momentWhimsy is fine on inbox zero. It is grating on a permissions failure, where the user is blocked and wants a route to whoever can unblock them.
loading → “No projects yet” → 12 projects
Do not show an empty state while loading"No projects yet" flashing before the data arrives tells the user their work has been deleted. Distinguish loading from empty, always.

Accessibility

Not a checklist to run at the end. These are the requirements the component was built from.

1.3.1Info and RelationshipsA2.4.6Headings and LabelsAA4.1.3Status MessagesAA

Contrast

  • The description uses --ds-fg-muted, the tightest pair in the system at 4.6:1. Do not go lighter for a "softer" empty state.
  • The icon is decorative and exempt, but if it carries meaning — an error icon — it must reach 3:1.

Keyboard

TabReaches the primary and secondary actions. The block itself is not focusable.
EnterActivates the focused action.

Screen readers

  • A sighted user sees the list is empty at a glance. A screen-reader user needs to be told — announce "No results" through the live region on the results container.
  • The heading gives the region a name in the landmarks list, which is how screen-reader users navigate between regions.
  • Do not announce the description as well as the title on every filter change. The title alone is enough.

Focus & touch

  • After a search returns nothing, keep focus in the search field. Moving it to the empty state means the user has to tab back to correct their query.
  • The primary action must be at least 44px and centred, which puts it in easy reach on mobile. Keep the secondary action below rather than beside it on narrow screens.
AttributeApplied toNotes
<h2> or <h3>The titleA real heading, so the region has an entry in the document outline.
aria-hidden="true"The iconDecorative. The title carries the meaning.
role="status"A no-results stateSo the change from 12 results to 0 is announced after a filter or search.
aria-live="polite"The results regionAnnounce the count, not the whole empty state, on every filter change.

Code

Example usage

tsx
1import { EmptyState } from '@/ui/Feedback'23// Always distinguish the four kinds — they need different actions4function DeploymentList({ items, query, filters, loading, error }) {5  if (loading) return <DeploymentSkeletons />          // never "empty"6  if (error) return <ErrorState error={error} onRetry={refetch} />78  if (items.length === 0) {9    const filtered = query || filters.length > 01011    return filtered ? (12      <EmptyState13        icon={<SearchX size={22} />}14        title={'No deployments match “' + query + '”'}15        description="Try a shorter term, or clear the environment filter."16        action={<Button onClick={clearFilters}>Clear filters</Button>}17      />18    ) : (19      <EmptyState20        icon={<FolderPlus size={22} />}21        title="No deployments yet"22        description="Connect a repository and we will build and deploy on every push."23        action={<Button onClick={connect}>Connect a repository</Button>}24        secondaryAction={<Button variant="text" onClick={demo}>Deploy a sample</Button>}25      />26    )27  }2829  return <List items={items} />30}3132// Announce the change for screen-reader users33<div aria-live="polite" className="sr-only">34  {items.length === 0 ? 'No results' : items.length + ' results'}35</div>

Framework-free HTML

html
<div class="ds-empty" role="status">
  <span class="ds-empty__icon" aria-hidden="true">
    <svg>…</svg>
  </span>
  <h3 class="ds-empty__title">No projects yet</h3>
  <p class="ds-empty__desc">
    A project holds your services, environments and deployments.
  </p>
  <div class="ds-empty__actions">
    <button class="ds-btn ds-btn--filled">Create a project</button>
    <button class="ds-btn ds-btn--text">Import from GitHub</button>
  </div>
</div>

CSS

css
.ds-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
  padding: 64px 32px;                  /* the emptiness is the point */
  text-align: center;
}
.ds-empty--compact { gap: 12px; padding: 40px 24px; }

.ds-empty__icon {
  display: grid;
  place-items: center;
  inline-size: 56px;
  block-size: 56px;
  border-radius: var(--radius-xl);
  border: 1px solid var(--ds-border-subtle);
  background: var(--ds-surface-inset);
  color: var(--ds-fg-muted);
}

.ds-empty__title { font-size: 1.1875rem; font-weight: 600; }

/* Centred text needs a narrower measure — every line starts somewhere new */
.ds-empty__desc {
  max-inline-size: 24rem;
  font-size: 13px;
  line-height: 1.6;
  color: var(--ds-fg-muted);
}

.ds-empty__actions { display: flex; gap: 8px; margin-block-start: 4px; }

/* Stack the actions on narrow screens rather than shrinking them */
@media (max-width: 480px) {
  .ds-empty__actions { flex-direction: column; inline-size: 100%; }
  .ds-empty__actions > * { inline-size: 100%; }
}

Component API

EmptyState

PropTypeDefaultDescription
title*string—Rendered as a real heading. Say what is empty and why.
descriptionReactNode—One or two sentences. Capped at 24rem.
iconReactNode—Small and decorative. Not an illustration.
actionReactNode—The one thing to do next. Omitting it creates a dead end.
secondaryActionReactNode—At most one. A third turns guidance into a menu.
tone'neutral' | 'danger''neutral'danger tints the icon container for a failure state.
compactbooleanfalseHalves the vertical padding. For use inside a card or table.

Notes

Professional tips

  • Write the empty state before you write the list. It forces you to articulate what the feature is for, and that sentence is usually the best copy in the product.
  • For a first-run state, offer a sample or a template alongside the real action. "Deploy a sample app" converts far better than a blank form.
  • Keep the search term in the title but truncate it at about 40 characters, or a pasted paragraph breaks the layout.
  • Inbox-zero states are one of the few places where a small piece of personality is genuinely welcome. Use it there and nowhere else.

Performance

  • Do not fetch anything from an empty state. It is often rendered dozens of times as filters change, and each render should be free.
  • Preload the destination of the primary action. The user is very likely to press it, and it is usually the heaviest route in the app.
  • Avoid animating the empty state in. It appears after a load and after every failed filter; animation makes both feel slower.

Common mistakes

  • Rendering the empty state while loading, which tells the user their data is gone.
  • The same generic copy for "no data" and "no results", so the action is wrong in one of the two cases.
  • No action at all, leaving the user with nowhere to go.
  • Moving focus into the empty state after a search, so the user has to tab back to fix their query.
  • A large illustration that pushes the action below the fold on a laptop.

Real-world recommendations

  • Empty states are the highest-converting onboarding surface in most products, and usually the least designed. They are worth a dedicated review.
  • Track how long new accounts sit at the first-run state. A long dwell time means the description is not explaining the concept.
  • For a no-results state, log the query. A recurring failed search is either a missing feature or a vocabulary mismatch, and both are cheap to fix.
  • When a filter combination can never return results, say so before the user applies it. Preventing the empty state beats designing it.