Browse all of Kata

Search and filter

Source docs/design/patterns/search-filter.mdMarkdown

On this page

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

Part of the Konjo design language. 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.

  • 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 — 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.