<!-- https://getkonjo.com/design/patterns/search-filter · source: docs/design/patterns/search-filter.md -->

# Search and filter

Finding one thing among many, and narrowing a list you are already looking at.

> Part of the [Konjo design language](../konjo-design-language.md). The laws in the spine
> apply here unless this chapter contradicts them.

Twenty files in `src/` render a search input and none of them share a component. This chapter
is what they should have agreed on.

## Search or filter — they are different

- **Search** takes a query and returns matches from a set the person cannot see all of.
- **Filter** narrows a set that is already on screen.

A screen can have both. It should never use one to do the other's job: a search field that
only filters ten visible rows is friction, and a filter chip row standing in for search across
four thousand records is a dead end.

## When a list gets search

- **Under ~15 items:** no search. The list is the interface.
- **15–60:** filter chips, if there is a meaningful axis. Otherwise nothing.
- **Over ~60, or unbounded:** search, and it goes **first** — above the filters, above the
  content.

## The field

```
[ ⌕ Search students…                    ✕ ]   h44 · radius.input · 1px border.control
( Brown belt ×) ( Needs waiver ×)             chips wrap, h32, never scroll sideways
```

- **`border.control`, not a fill.** A search field with only a `surface.tag` background is
  1.017:1 on the page and is not a control anyone can see.
- **The placeholder says what is searched**: "Search students…", not "Search". Never
  `text.quaternary` — it is not a text colour.
- **A leading `inline` magnifier**, and a clear affordance once there is a query, in a 44pt
  target.
- **`returnKeyType="search"`**, and searching does not dismiss the field.
- **Never autofocus** on arriving at a tab. The keyboard covering the content someone came to
  browse is the most hostile thing a list screen can do. Autofocus only on a screen whose sole
  purpose is search.

## Behaviour

- **Search as they type, debounced.** No search button.
- **Below ~200 local items, filter locally** and show results instantly. Do not round-trip a
  server for something already in memory.
- **The previous results stay visible while the next ones load.** Blanking the list on every
  keystroke makes a fast search feel broken.
- **Never reorder results while someone is still typing** in a way that moves the row under
  their thumb.

## Filter chips

- **Chips wrap; they never scroll horizontally.** A filter you cannot see is a filter you will
  forget is on — and horizontal scrolling hides exactly the ones at the end.
- **32pt tall, `radius.pill`, `surface.card` with a 1px `border.control`.** Selected fills
  `surface.hero` on `surface.card` with `text.onHero` — that is a control state, not a loud
  block, so it does not spend the screen's budget.
- **An active chip carries its own removal** (`×`) and `accessibilityState={{ selected }}`.
- **More than two active filters get a "Clear all"**, as a text link.
- **The count of what survived is stated**: "14 of 132". Without it, an over-filtered list is
  indistinguishable from an empty one.

## Empty results

The two empties are different and conflating them is the classic error.

| | What it means | What it shows |
|---|---|---|
| **Empty search** | The query matched nothing | Keep the field and the chips on screen. "No students match 'torez'." Offer the nearest real action: clear the query, or clear a filter that is doing the excluding. |
| **Empty collection** | There is nothing here at all | Replace the index entirely with the [empty state](states.md) — and *remove* the search field, because there is nothing to search. |

If filters are the reason, **say which filter**: "No students match 'ana' with *Needs waiver*
on." A person who cannot see why nothing matched assumes the app is broken.

## Accessibility

- `accessibilityRole="search"` on the field.
- Result counts announced with `accessibilityLiveRegion="polite"` — a sighted user sees the
  list change, a screen-reader user gets silence otherwise.
- Chips announce their selected state; a chip that only looks selected is invisible to half
  its users.

## Never

- Autofocus on a tab home.
- A search field that blanks its results on every keystroke.
- Horizontally scrolling filter chips.
- An empty search that hides the field that caused it.
- Filters with no count and no clear.
- A search input styled as a filled block with no border.
