Searching Versus Filtering a Data Grid

Permalink to "Searching Versus Filtering a Data Grid"

A text box above a grid can do two very different things. A search (or find) keeps every row and highlights the ones that match, letting the user step between matches in context. A filter removes every row that does not match, so the grid shrinks to just the results. Sighted users can tell which happened by looking at the scrollbar; screen reader users cannot, and a control that says “Search” but behaves as a filter leaves them unsure whether rows are missing or simply unread.

This guide explains how to choose, how to label and how to announce each behaviour. It belongs to table search and match highlighting.

Spec reference

Permalink to "Spec reference"

No single criterion says “use search” or “use filter”; the choice is shaped by several:

  • SC 2.4.6 Headings and Labels (AA) — the control’s label must describe its purpose. “Filter rows” and “Find in table” are descriptive; a bare “Search” is ambiguous in a grid.
  • SC 3.2.2 On Input (A) — typing that removes rows is a change of content, which is allowed, but must not move focus or open new contexts unexpectedly.
  • SC 4.1.3 Status Messages (AA) — both behaviours need a status message: a match count for search, a row count for filter.
  • SC 1.3.1 Info and Relationships (A) — after filtering, aria-rowcount (if used) must reflect the filtered total, not the original.
Search versus filter at a glance Comparison: search keeps every row and highlights matches, announces match position, keeps row count unchanged; filter removes non-matching rows, announces the new row count and changes aria-rowcount. Search versus filter at a glance✗ Search (find)Every row stays; matches highlightedStatus: "14 matches in 9 rows"Find next / previous moves focusaria-rowcount unchangedBest for inspecting matches in context✓ FilterNon-matching rows removedStatus: "Showing 9 of 240 rows"Focus stays in the field; user tabs to thegridaria-rowcount equals the filtered totalBest for working only with the matching subset

When to use — and when not to

Permalink to "When to use — and when not to"

Choose filter when the user’s next action applies to the matching rows only: bulk-selecting all overdue invoices, exporting the results, or reading through a short list. Filtering is easier to understand non-visually because the grid itself becomes the answer — a screen reader user who tabs into the table hears “table, 9 rows”.

Choose search when context matters: auditing every occurrence of an account code in a ledger, where the rows before and after a match are part of the meaning. Search also suits very large grids where filtering would be slow on the server but a client-side highlight is cheap.

The misapplication to avoid is a hybrid that filters and highlights, labelled “Search”, with no count. Users cannot tell whether the grid is showing everything. If you combine them, say so: “Filter rows (matches are highlighted)”, and announce the filtered count.

Choosing the behaviour A decision flow: will the user act only on matching rows? If yes, filter. Does the surrounding context matter? If yes, search. If both needs exist, provide two controls with distinct labels. Choosing the behaviourAct only on matching rows?bulk select, export, read the subsetyes: filterNeed neighbouring rows for meaning?ledgers, logs, audit trailsyes: search with find nextBoth needs are realtwo controls, two labels"Filter rows" and "Find in table"Neither is cleardefault to filtereasier to announce and to understand

Annotated code example

Permalink to "Annotated code example"
<!-- Filter: label says what happens to rows (SC 2.4.6) -->
<label for="row-filter">Filter rows</label>
<input id="row-filter" type="search" aria-describedby="row-filter-status" aria-controls="invoices">
<p id="row-filter-status" role="status">Showing 240 of 240 rows</p> <!-- SC 4.1.3 -->

<!-- Find: a separate control with its own verb -->
<label for="row-find">Find in table</label>
<input id="row-find" type="search" aria-describedby="row-find-status" aria-controls="invoices">
<button type="button" id="find-next">Next match</button>
<p id="row-find-status" role="status"></p>

<table id="invoices" aria-rowcount="241"> <!-- header row + 240 data rows; update after filtering (SC 1.3.1) -->
  ...
</table>
filterInput.addEventListener('input', debounce(() => {
  const visible = applyFilter(filterInput.value);       // removes non-matching rows from the DOM
  table.setAttribute('aria-rowcount', String(visible + 1));
  filterStatus.textContent = `Showing ${visible} of ${total} rows`; // count, not match position
}, 500));

aria-controls is included for completeness; JAWS can expose it as a jump command, but no reader relies on it, so the status message carries the real information.

Keyboard & AT behaviour

Permalink to "Keyboard & AT behaviour"
Control Event Expected announcement Deviation
Filter Typing pauses “Showing 9 of 240 rows” VoiceOver may read after a longer delay
Filter Tab into the grid “Invoices, table, 10 rows, 4 columns” JAWS reports rows including the header
Find Enter Cell content, then “Match 1 of 14” Status may be cut off in JAWS
Find Tab into the grid Full row count unchanged —
What a screen reader user hears when entering the grid Bar chart of the number of rows reported when tabbing into a 240-row grid after a filter (9 rows) versus a search (240 rows), and the number of matches that must be stepped through with find next (14). What a screen reader user hears when entering the gridAfter filtergrid reports 9 rows — the answer is the tableAfter searchgrid still reports every rowMatches to step throughreachable with Enter or Next match
The filter answer is in the table itself; the search answer lives in the match count.

Integration context

Permalink to "Integration context"

Filtering has its own topic with richer controls — facets, date ranges, multi-select — in accessible filtering and faceted search, and the count wording is covered in announcing filter result counts with aria-live. For the search side, see announcing match counts and find next and previous.

When a filter returns nothing, the grid should not simply vanish; give it a proper empty state as described in accessible empty states for filtered tables.

Gotchas

Permalink to "Gotchas"

Server-side filters with pagination. “Showing 25 of 240 rows” is misleading when 25 is the page size and 240 the filtered total. Say both: “240 matching rows, showing 1 to 25”.

Focus inside removed rows. If the user filters from a toolbar while a cell is focused, the focused row may disappear. Keep focus in the field, or move it to the first remaining row with an announcement.

Placeholder as label. “Search…” in a placeholder is not a label and disappears as soon as typing starts, leaving magnifier users without any reminder of which behaviour they are using.

Testing checklist

Permalink to "Testing checklist"

Design system notes

Permalink to "Design system notes"

Offer two distinct components, GridFilter and GridFind, rather than one GridSearch with a mode flag. Distinct names push product teams into choosing a behaviour on purpose, and each component can bake in its own status message and keyboard contract.

Framework notes

Permalink to "Framework notes"

In React, keep the filter query and the derived row list in the same component and compute the status string during render, writing it into a status element that is always mounted. A status element rendered conditionally ({count && <p role="status">…) is created and filled in one commit, which most screen readers ignore.

function GridFilter({ rows }) {
  const [q, setQ] = useState('');
  const dq = useDeferredValue(q);                       // keeps typing responsive on big grids
  const visible = useMemo(() => rows.filter(r => matches(r, dq)), [rows, dq]);
  return (<>
    <label htmlFor="f">Filter rows</label>
    <input id="f" type="search" value={q} onChange={e => setQ(e.target.value)} />
    <p role="status">{`Showing ${visible.length} of ${rows.length} rows`}</p> {/* always mounted */}
    <InvoiceTable rows={visible} />
  </>);
}

In Vue, a computed for the visible rows and a debounced watch that writes the status text gives the same result; in Angular, LiveAnnouncer from the CDK can replace the status element, but keep a visible count as well so sighted users get the same information.

FAQ

Permalink to "FAQ"
Should a grid have both a filter and a find box?

Only when users genuinely need both. Two text boxes with similar placeholders confuse everyone, so label them with different verbs and keep the find box near the grid’s toolbar.

Is "Search" ever the right label for a filter?

For a site-wide search that navigates to a results page, yes. Inside a grid toolbar, “Filter rows” is clearer, because it tells users the grid will change.

Does aria-controls help users find the grid after filtering?

Rarely. JAWS offers a command to jump to the controlled element, but most users simply press Tab. The status message and a predictable tab order matter far more.

Permalink to "Related"

← Back to Table Search & Match Highlighting