Zero-Results Suggestions and Recovery

Permalink to "Zero-Results Suggestions and Recovery"

“No results” is a dead end unless the interface helps users out of it. The best zero-results states tell users which filter is excluding everything and let them remove just that one: “Remove ‘Customer is Lund & Co’ (12 results)”. Sighted users scan such suggestions instantly. For screen reader users the same suggestions must be properly named buttons, with the predicted count in the name, and every action must report its result.

This guide extends accessible empty state for filtered tables and belongs to empty, error & no-results states.

Spec reference

Permalink to "Spec reference"
  • SC 3.3.3 Error Suggestion (AA) — where suggestions for correction are known, provide them. A misspelled search term is close to an input error, and a spelling suggestion satisfies this directly.
  • SC 2.4.6 Headings and Labels (AA) and SC 2.5.3 Label in Name (A) — the visible text “Remove Customer is Lund & Co” must be contained in the accessible name, even if you add the count.
  • SC 4.1.3 Status Messages (AA) — each recovery action reports its outcome.
  • SC 2.4.3 Focus Order (A) — removing a suggestion button must not leave focus on nothing.
Predicted results for each recovery option Bar chart of predicted row counts for each recovery option on a zero-result grid: remove the customer filter gives 12 rows, remove the status filter gives 3, remove the date range gives 0 and is marked as unhelpful, clear all gives 240. Predicted results for each recovery optionRemove Customer12 rows — the filter excluding most resultsRemove Status3 rowsRemove Date rangestill no results — do not offer as a suggestionClear all filtersevery invoice
Showing counts lets users pick the smallest change that helps.

When to use — and when not to

Permalink to "When to use — and when not to"

Offer per-filter removal when two or more filters are active; with one filter, “Clear filter” is enough. Offer predicted counts only when they are cheap and accurate — a stale count (“12 results” that becomes 0) is worse than none. Offer spelling suggestions for free-text queries when a near match exists in the data.

Do not offer suggestions that also lead to zero results. A list of four buttons, three of which also return nothing, sends users round in circles.

The misapplication: suggestion chips with only the filter value as their name (“Lund & Co”) and an “×” icon. Screen reader users hear “Lund & Co, button” and cannot tell whether pressing it applies, removes or edits the filter.

Ambiguous chips versus explicit recovery buttons Comparison of a chip named only by its value against a button whose name states the action, the filter and the outcome. Ambiguous chips versus explicit recovery buttons✗ Chip: "Lund & Co ×"Action unclear: apply or remove?Filter field not namedNo idea what happens next"×" read as "times" by some readers✓ Button: "Remove Customer is Lund & Co,12 results"Action stated: removeField and value namedPredicted outcome in the nameIcon hidden; text carries the action

Annotated code example

Permalink to "Annotated code example"
<td colspan="4" class="empty-cell">
  <p id="empty-msg">No invoices match all 3 filters.</p>
  <!-- A labelled list of recovery actions (SC 1.3.1) -->
  <ul aria-labelledby="empty-msg" class="recovery">
    <li>
      <button type="button" data-remove="customer">
        <!-- Visible text is contained in the name (SC 2.5.3); count adds outcome (SC 3.3.3) -->
        Remove <q>Customer is Lund &amp; Co</q> <span class="count">(12 results)</span>
      </button>
    </li>
    <li>
      <button type="button" data-remove="status">
        Remove <q>Status is Overdue</q> <span class="count">(3 results)</span>
      </button>
    </li>
  </ul>
  <p>Did you mean <a href="?q=lund" data-suggest="lund">Lund</a>?</p> <!-- spelling suggestion -->
  <button type="button" id="clear-all">Clear all filters</button>
</td>
recovery.addEventListener('click', (e) => {
  const btn = e.target.closest('button[data-remove]');
  if (!btn) return;
  removeFilter(btn.dataset.remove);
  const n = countRows();
  // Rows will replace the empty cell: put focus somewhere stable first (SC 2.4.3)
  (n ? firstRowCell() : document.getElementById('empty-msg')).focus();
  status.textContent = n
    ? `Removed filter. Showing ${n} invoices.`
    : 'Removed filter. Still no invoices match.';             // SC 4.1.3
});

Keyboard & AT behaviour

Permalink to "Keyboard & AT behaviour"
Event Expected announcement AT deviations
Tab into recovery list “List, 2 items. Remove ‘Customer is Lund & Co’ (12 results), button” VoiceOver may drop “list” in Safari when list styles are removed — add role="list"
Activate a removal Focus to first row; “Removed filter. Showing 12 invoices.” JAWS reads the row then the status
Activate spelling suggestion Page reloads or re-filters; “Showing 9 invoices for ‘lund’” —
Clear all filters “Filters cleared. Showing 240 invoices.” —
Removing one filter from the empty state Sequence between user, recovery button, grid and status: the user activates remove; the grid re-queries; focus moves to the first new row; the status announces the new count. Removing one filter from the empty stateUserRecovery buttonGridStatusactivate "Remove Customer…"drop customer filter,re-queryfocus first row"Removed filter. Showing 12invoices."
The predicted count and the announced count should match.

Integration context

Permalink to "Integration context"

The applied filters listed here are the same ones shown as chips in the toolbar; keep their naming identical to applied filter chips and removal so users recognise them. Spelling suggestions for search queries complement searching versus filtering a data grid. Count wording follows announcing filter result counts with aria-live.

Gotchas

Permalink to "Gotchas"

list-style: none in Safari. VoiceOver with Safari stops exposing unstyled lists as lists. Add role="list" to the ul when you remove bullets.

q elements and quotes. q renders quotation marks through CSS, which screen readers may or may not speak. That is acceptable; the filter name is still read. Avoid typing literal quotes as well, or they may be doubled.

Expensive count predictions. Predicting counts for every filter combination can multiply backend queries. Compute only “remove one filter” counts, and only for up to five filters.

Testing checklist

Permalink to "Testing checklist"

Framework notes

Permalink to "Framework notes"

Predicted counts are usually computed alongside the main query. With TanStack Table client-side filtering, compute them by running getFilteredRowModel logic against a copy of the column filters with one entry removed; memoise per filter. With server-side filtering, ask the API for “facet counts excluding this filter” in the same response — the pattern most search backends (Elasticsearch, OpenSearch, Algolia) support natively.

// Client-side: rows remaining if each single filter were removed
const predictions = activeFilters.map(f => ({
  filter: f,
  count: rows.filter(r => activeFilters.every(g => g === f || g.test(r))).length,
})).filter(p => p.count > 0);                          // never suggest another dead end

Design system notes

Permalink to "Design system notes"

Make recovery suggestions a slot of the no-results component that accepts { label, count, onSelect } items and renders them as a labelled list of buttons with consistent wording. Centralising the wording (“Remove ‘Field is Value’ (N results)”) keeps it identical to the filter chips elsewhere in the product.

FAQ

Permalink to "FAQ"
Should predicted counts be in the accessible name or a description?

In the name is simplest and keeps them together when users browse by button. If names get long, move the count to aria-describedby, but keep the visible text first so voice control users can say it.

Is a spelling suggestion a link or a button?

If it changes the URL and the result is shareable, a link. If it only re-filters in place, a button. Either way, announce the new result count.

What if no single filter removal produces results?

Offer “Clear all filters” and, if relevant, a saved view the user often uses. Do not list removal buttons that all lead to zero.

Should recovery suggestions be announced when the empty state appears?

Not individually. Announce the zero-results message with the active filters; users who want suggestions Tab to them. Reading every suggestion in the status message makes it too long to remember.

Can recovery suggestions include saved views?

Yes, and they are often the most useful option: “Open your view ‘My overdue invoices’ (14 results)”. Use the same button pattern and include the count when it is cheap to compute.

Permalink to "Related"

← Back to Empty, Error & No-Results States