Zero Result Rescue

Turns an empty search into fixes that are proven to return results: a spelling fix, a layout fix, a dropped filter or all categories.

StopsUsers quit on empty search results

Installation

pnpm dlx shadcn@latest add https://bollard-ui.vercel.app/r/zero-result-rescue.json

The files land in your project. They're yours to edit.

Usage

zero-result-rescue-usage.tsx
import { ZeroResultRescue, type SearchAttempt } from "@/components/ui/zero-result-rescue"

<ZeroResultRescue
  query={query}
  filters={filters}
  scope={allCategories ? undefined : category}
  vocabulary={vocabulary}
  count={count}
  onRescue={(attempt) => {
    setQuery(attempt.query)
    setBudgetOn(attempt.filters.includes(budget.id))
    setAllCategories(allCategories || attempt.allCategories)
  }}
/>

What it catches

  • Misspellings against your own vocabulary
  • Wrong keyboard layout and Caps Lock
  • The single filter that hides every result
  • A category scope that's too narrow
  • Only fixes with a non-zero count are shown

Props

ZeroResultRescue props
PropTypeDefault
query* (required)

The query that returned nothing.

string—
filters

Filters applied to that search.

ActiveSearchFilter[][]
scope

The category the search was limited to, if any.

string—
vocabulary

Words your catalog knows (product names, brands, categories), used for "did you mean".

string[][]
layouts

Keyboard layouts to try when the query looks typed with the wrong one.

KeyboardLayout[]DEFAULT_LAYOUTS
count* (required)

Returns how many results an attempt would give, or a promise of it (a count-only API call). Called once per candidate fix, all at once; the signal aborts when the query changes. A rejected count just drops that fix.

(attempt: SearchAttempt, signal: AbortSignal) => number | Promise<number>—
onRescue* (required)

Run the chosen fix.

(attempt: SearchAttempt, rescue: Rescue) => void—
limit

Most fixes to offer.

number3
titleText

Templates: `{query}`, `{scope}`, `{filters}`, `{count}`, `{filter}`, `{layout}`, `{from}`, `{to}` are filled in.

string"No results for “{query}”"
scopeText

Text naming the current category scope.

string"in {scope}"
searchLabel

Label prefix of a search-instead fix.

string"Search “{query}”"
withoutLabel

Label prefix of a remove-filter fix.

string"without {filter}"
allCategoriesLabel

Label of the search-all-categories fix.

string"in all categories"
spellingReason

Reason shown for a spelling fix.

string"“{from}” looks like “{to}”"
layoutReason

Reason shown for a keyboard-layout fix.

string"typed with the {layout} keyboard layout"
capsReason

Reason shown for a Caps Lock fix.

string"Caps Lock looks on"
filterReason

Reason shown for a filter fix.

string"{filter} hides every match"
scopeReason

Reason shown for a category fix.

string"nothing in {scope}"
fallbackText

Shown when no single change brings results back.

string"Nothing close turned up. Try fewer or more general words."
formatCount

Formats a result count.

(count: number) => stringdefaultCount
filtersText

"1 filter" / "2 filters" under the title.

(count: number) => string(n) => `${n} ${n === 1 ? "filter" : "filters"}`
checkingText

Shown while async counts are coming in.

string"Looking for close matches…"
countKey

Fixes are recounted when the query, filters, scope, vocabulary or layouts change; change this too when the data behind `count` does.

string | number—

* required. Also accepts the native props of <div> (className, aria-*, data-*…), passed through to it.

Accessibility

  • Region is role=status with aria-busy while counting
  • Fix buttons include their reason and count as text
  • Icons are aria-hidden

Only what the source does. Test with your own screen reader and content before shipping.

Limitations

  • Counts come from your count function; slow APIs delay suggestions
  • Did-you-mean is limited to the vocabulary you pass

What it stops

Users quit on empty search results

Also known as

no results · empty state · search · did you mean · zero results · filters · search page · ecommerce search

Field type

Dependencies

Pairs well with