Filtering data
A filter is just a small form that changes a list. Let people set several options first, then press Apply. When the list changes, say so in words, because a screen reader user cannot see the list jump.
In one minute
- Use native controls: a select for one of many, radios for one of few, checkboxes for some of few.
- Apply in a batch with a button, so a keyboard user can change three options without the list reshuffling three times.
- Announce the result with
role="status"("Showing 2 of 4 routes"), or move focus to the results heading. - Keep the filter in the URL and name pagination buttons "Page 2", not just "2".
Think of ordering at a counter. You say "small, oat milk, no sugar", then the barista starts. If the barista began making a drink at the word "small" and restarted at every change, you would be talking over the noise. A filter that updates on every keystroke does that to a screen reader.
1. Pick the right native control
Browsers already ship controls that work with the keyboard, touch and screen readers. Choose by asking two questions:
how many options are there, and can the user pick more than one? Related checkboxes or radios belong in a
<fieldset> with a <legend>, so the question is read along with the answers.
Lab: which control fits?
Step 1. Choose the situation. The matching control appears. Try it with the keyboard.
Avoid custom dropdowns made of divs unless you rebuild everything the native select gives you. A multi-select
<select multiple> needs Ctrl or Shift to choose several items, which many people do not know, so
checkboxes are friendlier for a short list.
2. Apply in a batch, not on every change
On a closed select, pressing ↓ can change the value (and fire a change event) in some
browsers. If the list reloads on each change, a keyboard user who wants "Coast" passes through "North" first and
triggers extra updates. WCAG 3.2.2 (On Input) says changing a setting must not cause a surprise change of context
unless the user was told first. An Apply button avoids all of this.
Lab: count the interruptions
Step 2. Click the region select, then press ↓ a few times. Compare both versions.
Announcements the screen reader would make: 0
3. Say what happened
After the list changes, sighted people see it. A screen reader user hears silence. There are two good fixes. A
role="status" region is a polite announcement: it waits until the user stops speaking and does not move
focus. Moving focus to the results heading (give it tabindex="-1") is the better choice when the list is
the very next thing the user wants to read. Do not do both, or the user hears it twice.
Lab: filter and listen
Step 3. Choose how results are announced, pick a region, press Apply. Try "None" as the region too: it gives zero results.
Results
Always handle the empty case with words ("No routes match. Try another region.") and a way to clear the filters. An empty list with no text looks like a broken page.
4. Pagination and the URL
Page numbers are links or buttons, so they need names that make sense out of context. A button that says only "2"
is heard as "2, button". Add aria-current="page" to the current one. Put the filters and the page in
the URL (?region=north&page=2), so the Back button, a refresh and a shared link all work.
Lab: name the page buttons
Step 4. Switch the naming, then press a page button.
Common mistakes
| You see | Why it is a problem | Fix |
|---|---|---|
| List reloads on every change | Keyboard users trigger many updates; a screen reader repeats itself. | Apply button, or debounce and announce once. |
| No message after filtering | Screen reader users do not know anything changed. | role="status" or focus the results heading. |
| Both status and focus move | The result is announced twice. | Choose one. |
| Empty list, no text | Looks like an error or a loading failure. | "No routes match" + a Clear filters button. |
| Checkbox group with no legend | Options are heard without the question. | fieldset + legend. |
| Page buttons named "1, 2, 3" | No context in a list of buttons. | "Page 2" + aria-current="page". |
| State only in JavaScript | Back, refresh and sharing lose the filters. | Keep it in the query string. |
- In lab 2, use only the keyboard: Tab to the select, press ↓ twice. How many announcements did each version make?
- In lab 3, choose "None (no matches)" and apply. Is the empty result explained?
- Sort controls belong on table columns. The next lesson does that.
Remember
- Native controls first, grouped with
fieldsetandlegend. - Apply in a batch so the user stays in control.
- Announce the count with
role="status"or move focus to the results heading, not both. - Name page buttons, mark the current page, keep state in the URL.