Accessibility pattern · Feedback and status

Empty state

An empty state is a heading that says what is missing, a sentence on why, and the action that fills it; the drawing is decoration and stays out of the accessibility tree. When a list empties or fills because of something the person did, focus moves to whatever now answers them, and a polite status message covers changes they did not cause.

WCAG criteria
6
Keyboard rules
7
Checked with
axe, keyboard and the inspector

Live demo

Try it

Use it with a mouse, a keyboard or a screen reader. The inspector beside it shows what the browser tells assistive technology as you go: focus, state changes and announcements.

Fabric search

Filters

No results for linnen

Check the spelling, or clear the filters to see more fabrics.

Did you mean linen?

Inbox

Your inbox is empty

Questions from customers land here, and we email you when one arrives.

Projects

No projects yet

A project keeps the fabrics, measurements and notes for one order together.

The demo works without JavaScript only as far as its HTML does; the inspector needs JavaScript.

Keyboard

Keys it answers to

Every action works without a pointer.

KeyWhat it does
TabMoves through the sample-data switch, the search, the filter, the empty states' links and buttons, and any items in the lists.
SpaceOn Show sample data, fills every list or empties it again; a status message says which.
EnterOn Did you mean, searches for the suggestion and moves focus to the result count, or to the empty state if there is still nothing.
Enter or SpaceOn Clear filters or a filter chip, removes the filters and moves focus the same way.
Enter or SpaceOn Create a project, swaps the empty state for a short form and moves focus to its name field; Cancel brings focus back.
EscapeIn the new-project form, closes it like Cancel and returns focus to the button that opened it.
Enter or SpaceOn Archive, removes that message; focus moves to the next message, or to the empty inbox's heading after the last one.

Screen readers

What it announces

Written from the roles, names and states in the markup.

WhenExpected announcement
Focus moves to the search's empty stateNo results for linnen, heading level 4
Did you mean linen is followed1 fabric found, heading level 4
The last message is archivedMessage archived. Your inbox is empty, heading level 4
Create a project is pressedProject name, edit text
Show sample data is switched onShow sample data, switch, on. Showing sample data
A search is sent with nothing to findNo fabrics found

These are expected announcements, not recordings. Wording and order differ between screen readers and browsers.

Code

Copy the code

The exact files this demo runs on. The styles are served with a prefix that keeps this site's own styles out of the demo; what you copy is the original.

The markup

<div class="ap-empty" data-ap-empty-state>
  <div class="ap-empty__demo">
    <label class="ap-empty__switch">
      <input type="checkbox" role="switch" data-ap-sample />
      <span>Show sample data</span>
    </label>
  </div>

  <section class="ap-empty__card ap-empty__card--wide" aria-labelledby="empty-search-name">
    <div class="ap-empty__head">
      <h3 class="ap-empty__card-name" id="empty-search-name">Fabric search</h3>
    </div>
    <form class="ap-empty__search" role="search" aria-label="Fabrics" data-ap-search>
      <label class="ap-empty__vh" for="empty-q">Search fabrics</label>
      <input class="ap-input" id="empty-q" type="search" name="q" value="linnen" autocomplete="off" spellcheck="false" />
      <button type="submit" class="ap-btn">Search</button>
    </form>
    <div class="ap-empty__filters" data-ap-filters>
      <span class="ap-empty__filters-label" id="empty-filters-label">Filters</span>
      <ul aria-labelledby="empty-filters-label">
        <li data-ap-filter="under500">
          <button type="button" class="ap-empty__chip" data-ap-remove>
            <span class="ap-empty__vh">Remove filter:</span>
            <span>Under ₹500</span>
            <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M7 7l10 10M17 7 7 17"/></svg>
          </button>
        </li>
      </ul>
    </div>

    <div class="ap-empty__body">
      <h4 class="ap-empty__count" tabindex="-1" data-ap-count hidden><span data-ap-n>0</span> <span data-ap-unit>fabrics found</span></h4>
      <ul class="ap-empty__list" data-ap-results hidden></ul>
      <div class="ap-empty__state" data-ap-none>
        <svg class="ap-empty__art" viewBox="0 0 120 88" aria-hidden="true" focusable="false"><rect class="ap-empty__art-fill" x="14" y="18" width="70" height="52" rx="10"/><path class="ap-empty__art-soft" d="M26 34h34M26 46h22M26 58h28"/><circle class="ap-empty__art-glass" cx="78" cy="44" r="17"/><path class="ap-empty__art-line" d="m90.5 56.5 13 13"/><path class="ap-empty__art-line" d="M74 39.5a4.5 4.5 0 1 1 6 4.2c-1.3.5-2 1.4-2 2.8v.5M78 51.5h.01"/></svg>
        <h4 class="ap-empty__name" tabindex="-1"><span>No results for</span> <q translate="no" data-ap-q>linnen</q></h4>
        <p class="ap-empty__text" data-ap-why="filters">Check the spelling, or clear the filters to see more fabrics.</p>
        <p class="ap-empty__text" data-ap-why="spelling" hidden>Check the spelling, or try a broader word such as cotton or silk.</p>
        <p class="ap-empty__suggest" data-ap-suggest-line><span>Did you mean</span> <a href="#empty-q" translate="no" data-ap-suggest>linen</a>?</p>
        <div class="ap-empty__actions">
          <button type="button" class="ap-btn" data-ap-clear-filters>Clear filters</button>
        </div>
      </div>
    </div>
    <template data-ap-catalogue>
      <li class="ap-empty__item" data-words="washed linen indigo" data-price="720">
        <span class="ap-empty__swatch ap-empty__swatch--accent" aria-hidden="true"></span>
        <span class="ap-empty__item-text"><span class="ap-empty__item-name">Washed linen, indigo</span> <span class="ap-empty__item-meta">₹720 a metre</span></span>
      </li>
      <li class="ap-empty__item" data-words="linen kurta fabric natural" data-price="640">
        <span class="ap-empty__swatch ap-empty__swatch--natural" aria-hidden="true"></span>
        <span class="ap-empty__item-text"><span class="ap-empty__item-name">Linen kurta fabric, natural</span> <span class="ap-empty__item-meta">₹640 a metre</span></span>
      </li>
      <li class="ap-empty__item" data-words="cotton linen blend rust" data-price="480">
        <span class="ap-empty__swatch ap-empty__swatch--rust" aria-hidden="true"></span>
        <span class="ap-empty__item-text"><span class="ap-empty__item-name">Cotton-linen blend, rust</span> <span class="ap-empty__item-meta">₹480 a metre</span></span>
      </li>
      <li class="ap-empty__item" data-words="handloom cotton white" data-price="320">
        <span class="ap-empty__swatch ap-empty__swatch--white" aria-hidden="true"></span>
        <span class="ap-empty__item-text"><span class="ap-empty__item-name">Handloom cotton, white</span> <span class="ap-empty__item-meta">₹320 a metre</span></span>
      </li>
      <li class="ap-empty__item" data-words="chanderi silk gold" data-price="1150">
        <span class="ap-empty__swatch ap-empty__swatch--gold" aria-hidden="true"></span>
        <span class="ap-empty__item-text"><span class="ap-empty__item-name">Chanderi silk, gold</span> <span class="ap-empty__item-meta">₹1150 a metre</span></span>
      </li>
    </template>
  </section>

  <section class="ap-empty__card" aria-labelledby="empty-inbox-name">
    <div class="ap-empty__head">
      <h3 class="ap-empty__card-name" id="empty-inbox-name">Inbox</h3>
    </div>
    <ul class="ap-empty__list" data-ap-inbox hidden></ul>
    <div class="ap-empty__state" data-ap-inbox-none>
      <svg class="ap-empty__art" viewBox="0 0 120 88" aria-hidden="true" focusable="false"><path class="ap-empty__art-fill" d="M22 48h22l5 9h22l5-9h22v20a8 8 0 0 1-8 8H30a8 8 0 0 1-8-8Z"/><path class="ap-empty__art-line" d="M22 48 34 20h52l12 28M22 48v20a8 8 0 0 0 8 8h60a8 8 0 0 0 8-8V48H76l-5 9H49l-5-9Z"/><path class="ap-empty__art-soft" d="m52 30 6 6 12-12"/></svg>
      <h4 class="ap-empty__name" tabindex="-1">Your inbox is empty</h4>
      <p class="ap-empty__text">Questions from customers land here, and we email you when one arrives.</p>
    </div>
    <template data-ap-sample-inbox>
      <li class="ap-empty__item">
        <span class="ap-empty__avatar" aria-hidden="true">RS</span>
        <div class="ap-empty__item-text" id="empty-msg-1">
          <p class="ap-empty__item-name">Ritu Sharma</p>
          <p class="ap-empty__item-meta">Is the indigo linen pre-washed?</p>
        </div>
        <span class="ap-empty__time">9:40</span>
        <button type="button" class="ap-empty__archive" data-ap-archive aria-describedby="empty-msg-1">Archive</button>
      </li>
      <li class="ap-empty__item">
        <span class="ap-empty__avatar" aria-hidden="true">IQ</span>
        <div class="ap-empty__item-text" id="empty-msg-2">
          <p class="ap-empty__item-name">Imran Qureshi</p>
          <p class="ap-empty__item-meta">Can I order 3.5 metres of the rust blend?</p>
        </div>
        <span class="ap-empty__time">Mon</span>
        <button type="button" class="ap-empty__archive" data-ap-archive aria-describedby="empty-msg-2">Archive</button>
      </li>
    </template>
  </section>

  <section class="ap-empty__card" aria-labelledby="empty-projects-name">
    <div class="ap-empty__head">
      <h3 class="ap-empty__card-name" id="empty-projects-name">Projects</h3>
      <button type="button" class="ap-btn ap-empty__new" data-ap-create hidden>New project</button>
    </div>
    <ul class="ap-empty__list" data-ap-projects hidden></ul>
    <div class="ap-empty__state" data-ap-projects-none>
      <svg class="ap-empty__art" viewBox="0 0 120 88" aria-hidden="true" focusable="false"><path class="ap-empty__art-fill" d="M18 26a6 6 0 0 1 6-6h20l8 8h44a6 6 0 0 1 6 6v36a6 6 0 0 1-6 6H24a6 6 0 0 1-6-6Z"/><path class="ap-empty__art-line" d="M18 26a6 6 0 0 1 6-6h20l8 8h44a6 6 0 0 1 6 6v36a6 6 0 0 1-6 6H24a6 6 0 0 1-6-6Z"/><path class="ap-empty__art-line" d="M60 42v18M51 51h18"/></svg>
      <h4 class="ap-empty__name" tabindex="-1">No projects yet</h4>
      <p class="ap-empty__text">A project keeps the fabrics, measurements and notes for one order together.</p>
      <div class="ap-empty__actions">
        <button type="button" class="ap-btn ap-btn--primary" data-ap-create>Create a project</button>
      </div>
    </div>
    <form class="ap-empty__form" data-ap-form novalidate hidden>
      <div class="ap-field">
        <label class="ap-label" for="empty-project">Project name</label>
        <input class="ap-input" id="empty-project" name="project" type="text" autocomplete="off" />
        <p class="ap-error" id="empty-project-error" hidden>Enter a name for the project.</p>
      </div>
      <div class="ap-empty__actions ap-empty__actions--start">
        <button type="submit" class="ap-btn ap-btn--primary">Create</button>
        <button type="button" class="ap-btn" data-ap-cancel>Cancel</button>
      </div>
    </form>
    <template data-ap-sample-projects>
      <li class="ap-empty__item">
        <span class="ap-empty__swatch ap-empty__swatch--folder" aria-hidden="true"><svg viewBox="0 0 24 24" focusable="false"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z"/></svg></span>
        <span class="ap-empty__item-text"><a class="ap-empty__item-name" href="#empty-projects">Wedding sherwani, Rohan</a> <span class="ap-empty__item-meta">4 fabrics</span></span>
      </li>
      <li class="ap-empty__item">
        <span class="ap-empty__swatch ap-empty__swatch--folder" aria-hidden="true"><svg viewBox="0 0 24 24" focusable="false"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z"/></svg></span>
        <span class="ap-empty__item-text"><a class="ap-empty__item-name" href="#empty-projects">Office kurtas, Sana</a> <span class="ap-empty__item-meta">2 fabrics</span></span>
      </li>
    </template>
  </section>

  <p class="ap-empty__vh" role="status" data-ap-live></p>
</div>

WCAG 2.2

What it meets

The success criteria this pattern takes care of, and how.

  • 1.1.1 Non-text Content Level A

    The drawings are aria-hidden: they repeat what the heading says, so a screen reader skips them instead of reading "image".

  • 1.3.1 Info and Relationships Level A

    Each empty state starts with a real heading, so it is found in the headings list and read as the answer to what is in the card.

  • 2.4.3 Focus Order Level A

    When the control someone used disappears, focus moves to the result count, the empty state's heading or the next message, never to the top of the page.

  • 2.4.6 Headings and Labels Level AA

    Headings say exactly what is missing ("No results for linnen", "No projects yet") rather than a generic "Nothing here".

  • 3.3.3 Error Suggestion Level AA

    A search with no results suggests a corrected spelling and offers to clear the filters that may be hiding results.

  • 4.1.3 Status Messages Level AA

    Search results and the sample-data switch are confirmed in a polite status message, without moving focus from the field or switch.

Usage

When to use it

Use it

  • A list, table or search that can legitimately have nothing in it: a new account, an inbox at zero, a search with no matches.
  • First use, when the empty view is the best place to explain what the feature is for and how to start.

Use something else

  • Content that is still loading: show a loading state, or the empty message appears and then vanishes.
  • Errors: a failed request is not an empty list, and saying "No results" hides the problem. Use an error message.

Common failures

How it usually goes wrong

  • A blank area with no words

    An empty container gives sighted people a gap and screen reader users nothing at all. Say what is missing in a heading.

  • An illustration with alt text

    "Illustration of an empty tray" adds nothing to "Your inbox is empty" and makes everyone listen to it. Decorative drawings are aria-hidden.

  • Focus lost when the last item goes

    Archiving the last message removes the button that had focus, and focus falls to the top of the page. Here it moves to the empty state's heading.

  • No way forward

    "No results" with nothing else is a dead end. Offer a spelling suggestion, a way to clear filters, or the action that creates the first item.

  • Empty shown while loading

    Flashing "No projects yet" before the data arrives tells people their work is gone. Show the empty state only once the list is known to be empty.

  • Silent changes

    When a list empties or fills without the person's own action, a screen reader user has no way to know. A polite status message says so.

Notes

Building it

  • Keep the heading level in step with the page: the empty state's heading sits one level below the card's heading, h4 here.
  • Give the heading tabindex="-1" so a script can move focus to it when the list empties; it stays out of the Tab order.
  • One primary action is enough. If the empty view has several, people stop at the first; put the rest in the normal page.
  • The switch at the top is a demo control for watching the transitions; in a real product the data decides which view shows.
  • The did-you-mean link points at the search field, so it still makes sense without JavaScript; the script runs the corrected search instead.

Sources: WCAG 2.2 Understanding 4.1.3: Status Messages · WCAG 2.2 Understanding 2.4.3: Focus Order

Checked with axe in light and dark themes, at desktop and phone widths, and by keyboard. Not yet tested with every screen reader and browser pair. Report a correction