search-filter
Search and Filtering
Search is a conversation the user is losing by default: they type an approximation of what they want, and the interface either narrows toward it or leaves them guessing why it did not. Default posture: keep the query state fully visible and fully reversible, keep the input under the user's fingers no matter what the results do, and treat zero results as a recovery surface rather than an ending. Filtering destroys information, so every applied filter must be visible where it can be removed — most "your search is broken" reports are an invisible filter still doing its job.
The boundary with the sibling that looks like this one: the grid, its columns, and its row density are dense-ui; the query, the facets, and how results are presented are here. Two more handoffs: ui-states owns the anatomy and thresholds of loading, empty, and error surfaces — this skill only decides which of the three an outcome is — and navigation owns the URL-as-state contract filter state obeys.
Use the query and combobox layers the project already has. Check how URL state is managed (URLSearchParams, the router's search params, nuqs, a loader), how server state is fetched and cancelled (TanStack Query, SWR, a wrapper with AbortController), and which combobox primitive is installed (Radix, Headless UI, cmdk, Downshift). A hand-rolled listbox beside an installed one is two keyboard contracts to maintain, and one will be wrong.
Quick Reference
| Open it when | File |
|---|---|
| You know the surface — command palette, faceted catalog, table filter bar, log search, typeahead — and need its recipe for trigger, filter surface, URL keys, zero-result recovery, and keyboard map | patterns.md |