Search is a conversation, not a single shot. The user types a guess, sees what comes back, and refines. The interface's job is to make each turn of that loop fast and legible: accept the query without friction, return useful results quickly, and when nothing matches, help the user reformulate instead of dead-ending them. A search box that only works when the user already knows the exact term isn't search, it's a password prompt.
The query is the start of the dialogue, the results are the reply, and the empty result is the most important moment of all, because that's where users abandon. Design all three, not just the happy path.
The Search Input
The input sets the tone. Get the basics right and the rest of search has a foundation.
- Use
type="search"so the browser exposes search semantics, a native clear affordance, and a search-labeled return key on some browsers (seeforms.md— Use Correct Input Types).type="search"does not change the on-screen keyboard; setinputmodefor that (seeforms.md— Use inputmode for Keyboards). - Provide a clear button (×) once there's a query, so the user can reset in one click without selecting-and-deleting. Give it an accessible name (see
review-rules.md— Icon-only buttons missing aria-labels). - Show an example in the placeholder, not just "Search." "Search orders, customers, SKUs…" tells the user what's searchable. End with an ellipsis to signal emptiness (see
forms.md— Placeholder Patterns). The placeholder is not a label; pair the input with a real (possibly visually-hidden) label. - Never block typing. Even if the query syntax is constrained, let the user type freely and reflect validity in the results, not by swallowing keystrokes (see
forms.md— Don't Block Typing). - Debounce, don't drop. For instant search, wait for a short pause (~150–300ms) before firing the request so you're not querying on every keystroke, but never lose characters the user typed during the wait.
Autocomplete & Suggestions
Autocomplete turns search from recall into recognition. Instead of remembering the exact term, the user picks from what the product offers as they type.
- Suggest as the user types, ranked by relevance, and keep the list short enough to scan (typically ≤ 8). A suggestion list longer than the screen is a second problem, not a help.
- Make it fully keyboard-navigable: ↑↓ to move through suggestions, Enter to choose, Escape to dismiss, with the input retaining the typed text (see
interactions.md— Keyboard Navigation in Lists). Mouse and keyboard must reach the same outcomes. - Group by category when results span types: "Customers," "Orders," "Pages." Headers let the user jump to the kind of thing they meant.
- Highlight the matched substring in each suggestion so the user sees why it matched.
- Offer recent searches when the field is focused but empty, so returning users can re-run a past query in one click (see Recent & Saved Searches below).
// Combobox: input owns the text, listbox holds suggestions, arrow keys move a virtual cursor
<input
type="search"
role="combobox"
aria-expanded={open}
aria-controls="search-suggestions"
aria-activedescendant={activeId} // points at the highlighted option, focus stays in the input
/>
<ul id="search-suggestions" role="listbox">
{suggestions.map((s) => (
<li key={s.id} id={s.id} role="option" aria-selected={s.id === activeId}>
{s.label}
</li>
))}
</ul>
Instant vs Submit Search
Two models, chosen by cost and intent.
- Instant (search-as-you-type): results update on each debounced keystroke. Best when results are cheap to fetch and the user benefits from steering mid-query (filtering a list, finding a known item). Keep prior results visible while the next set loads (see Loading & Latency below).
- Submit (search-on-enter): results appear only when the user commits. Best for expensive queries, full-page result sets, or when each query has real cost (a paid API, a heavy backend). The Enter key submits, per standard form behavior (see
forms.md— Enter Key Submits). - You can combine them: instant suggestions in the dropdown, full results on submit. The dropdown helps the user aim; Enter runs the real search.
Don't fire an instant search on a single character unless the dataset is tiny; one letter rarely narrows anything and wastes requests.
Results
The results are the product's answer. Make the answer legible.
- Rank by relevance, and make the basis visible enough that the order doesn't feel random. If results are sorted by something other than relevance (date, price), say so and let the user change it.
- Highlight matched terms in each result so the user sees why it surfaced. This is the single highest-value result-list detail.
- Show a result count ("128 results") so the user knows the scope, using
tabular-numsso the figure doesn't jitter as it updates (seeui/typography.md— Tabular Numbers). Announce the count to assistive tech (see Keyboard & Accessibility below). - Make each result scannable: lead with the matched/most-identifying field, keep secondary detail quieter (see
ui/hierarchy.md). A result row is a place to combine data and description, not force everything into uniform columns (seeui/polish.md— Think Outside the Box, Tables). - Paginate or virtualize long result sets, and persist the page in the URL (see Filters & Facets below).
Zero Results
Zero results is the moment search most often fails the user, and the easiest to fix. An empty result is never a dead end.
- Name the cause and offer a way forward, the filtered-out empty state pattern: "No results for 'reciept'. Check spelling or try a different term." (see
ui/polish.md— empty state type 2, filtered-out). - Suggest a correction ("Did you mean receipt?") when you can detect a likely typo. A one-click correction recovers the user instantly.
- Offer to broaden: clear a filter, remove a term, widen the date range. The next action undoes the cause of the emptiness (see Filters & Facets below).
- Never show first-use copy here. "No orders yet" when the catalog has thousands but none match the query is a lie that makes the product look broken (see
errors.md— distinguishing empty from failed, andmicrocopy.md— Empty State Copy). - Keep the search input and filters visible so the user can immediately reformulate. This is the one empty state that keeps its chrome (see
ui/polish.md— Hide chrome… Exception: the filtered-out state keeps its filters/search).
The rule: an empty result is never a dead end. Name the cause and hand the user the next move.
Filters & Facets
Filters and search work together: search narrows by query, filters narrow by attribute. Treat them as one system.
- Reflect active filters and the query in the URL so a search is shareable, refreshable, and survives Back/Forward (see
interactions.md— URL as State, Deep-Link Everything). A filtered search the user can't link to is half-built. - Show active filters as removable chips so the user can see and undo each constraint. The path out of zero results often runs through these chips.
- Keep filters legible at a glance: show counts per facet where it helps ("In stock (42)"), and don't offer a filter value that would return nothing.
- When filters produce zero results, the zero-results state explains which constraint emptied the set and offers to clear it (see Zero Results above).
Loading & Latency
Search latency is felt sharply because the user is waiting on their own input. Show honest progress and preserve context.
- Keep prior results visible while the next set loads. Don't blank the list to prove something is happening; dim it slightly or show a lightweight inline indicator and swap when ready (see
interactions.md— Honest Loading States, Background refresh; and Choosing a Loading Pattern). - Pick the loading pattern by what's unknown. A first search with no prior results may warrant a skeleton; a refine over existing results should keep them and update in place (see
interactions.md— Choosing a Loading Pattern). - Don't show a spinner for an instant local filter. If the filtering is client-side and immediate, loading chrome is theater (see
interactions.md— the "Nothing" loading path). - Handle the slow and failed cases honestly: a search that times out gets a real error with a retry, never an endless spinner (see
errors.md— Network & Async Errors).
Recent & Saved Searches
Search has memory worth surfacing. Most users re-run queries.
- Show recent searches when the input is focused and empty, so a repeat query is one click. Let the user clear their history.
- Let users save or pin frequent searches or filter combinations when the product supports it, so a complex query becomes reusable.
- Recent and saved searches are navigation shortcuts; keep them quiet and out of the way until the field is focused.
Keyboard & Accessibility
Search is a combobox pattern, and comboboxes are easy to get subtly wrong. Follow the WAI-ARIA authoring pattern.
- The input is a
role="combobox"controlling arole="listbox"ofrole="option"suggestions, wired witharia-expanded,aria-controls, andaria-activedescendantso focus stays in the input while the active option updates (see the code above). - Announce the result count with a polite live region so screen-reader users learn how many results a query returned without leaving the input (see
interactions.md— Accessibility Announcements). - Every flow works by keyboard: type, arrow through suggestions, Enter to select, Escape to dismiss, and reach results without a mouse.
- Don't trap focus in the suggestion list; Tab should move on naturally, and dismissing suggestions returns the user to normal flow.
- The clear (×) button and any icon-only controls have accessible names (see
review-rules.md).
Search vs Command Palette
Search and a command palette look similar (a text box, a result list) but do different jobs. Don't conflate them.
- Search finds content in the product: records, documents, items. It answers "where is the thing about X."
- A command palette runs actions and jumps to destinations for users who know the product. It answers "do X" or "go to X" (see
navigation.md— Command Palette). - They can coexist, and a palette can include search results, but the palette is a power-user accelerator layered over navigation, while search is the primary way anyone finds content. Keep visible, browsable search for users who don't know the ⌘K shortcut exists.
The rule: search finds content, the palette runs actions. Don't collapse the two into one box.
Checklist
Input
-
type="search"with a real (or visually-hidden) label - Clear (×) button with an accessible name once there's a query
- Placeholder shows a searchable example, ends with ellipsis
- Typing never blocked; instant search debounced (~150–300ms) without dropping input
Suggestions & results
- Autocomplete keyboard-navigable (↑↓/Enter/Escape), matches highlighted, grouped by type
- Results ranked by relevance with matched terms highlighted
- Result count shown with
tabular-numsand announced via live region - Long result sets paginated/virtualized, page persisted in URL
Zero results & filters
- Zero results names the cause, suggests corrections, offers to broaden
- Never first-use copy on a filtered-empty result
- Search input and filters stay visible at zero results
- Active filters + query reflected in the URL; removable filter chips
Loading & memory
- Prior results kept visible while the next set loads
- Loading pattern matches what's unknown (skeleton vs in-place vs nothing)
- Timeouts resolve to an error + retry, never an endless spinner
- Recent searches surfaced on focus; saved searches where supported
Accessibility & scope
- Combobox roles wired (
combobox/listbox/option,aria-activedescendant) - Every flow operable by keyboard; focus not trapped in suggestions
- Search (find content) kept distinct from command palette (run actions)