Search is how somebody skips your information architecture, and W3C names the group it helps most. People with cognitive limitations may find it easier to use a search feature than to work their way down a hierarchy. That is a better reason to build it well than any argument about screen readers, and it is one you can cite.
It is also three problems wearing one coat. The field is a form. The results count is a status message. The results are a list. Each has its own rules and most implementations solve the first one and stop.
And there is no reference implementation to copy. The Authoring Practices Guide covers thirty component patterns and site search is not among them. It describes the search landmark, which names the region and says nothing whatever about the widget sitting inside it, so everything below is assembled from the criteria rather than lifted from one page.
The Field
A magnifying glass and a placeholder is the common build, and it fails twice. The placeholder disappears the moment somebody types, so anybody interrupted has lost the prompt. The icon-only submit button announces as button, with nothing saying what it does.
- Give the input a real label. Visually hidden is fine. That satisfies 3.3.2 Labels or Instructions, and it also matters for a criterion the label rule does not cover. A field with no accessible name fails 4.1.2 Name, Role, Value, and 4.1.2 is what an audit report will actually cite, because a placeholder is a last-resort fallback in the name computation and often gets the field past an automated name check while failing everybody who uses it.
- Use
type="search"and wrap it in aform. The form is what makes Enter submit with no JavaScript. The type is what gives the field a searchbox role in the accessibility tree and the browser's own clear button. - Give the submit button text, hidden visually if the design wants only an icon.
- Wrap the whole search control in a
searchelement, which defines a search landmark on its own with no ARIA at all. It has been widely available since October 2023, and W3C now lists the element first withrole="search"on the form kept as the fallback for when you cannot use it.
One boundary on that landmark, because it is easy to draw it too wide. The search element is not for presenting results. The field, the submit button, and the quick-suggestion list that drops out of the field all belong inside it, because those are search functionality. The results themselves belong in the page's main content. A filter panel that narrows the results is search functionality too, so that goes inside. And where a page has two search landmarks, such as a site search in the header and a filter search in the body, each one needs its own unique label.
There is a conformance argument for search that almost nobody makes to a client. 2.4.5 Multiple Ways at Level AA asks for more than one route to a page, and a site search function is one of the six routes W3C names. Search plus a site map satisfies a Level AA criterion outright, which turns a feature request into a compliance line item. The same criterion has a scope limit worth knowing in the same breath. A page that is the result of, or a step in, a process is excepted, and W3C's own worked example of that is a page of search results. So the results page itself is outside the rule you just satisfied by building the search.
Announce the Results
Results appear, and a screen reader user hears nothing. They do not know whether the search ran, whether it found nothing, or whether it is still going. A live region announcing 12 results for accessibility audit resolves all three, and it is almost word for word one of W3C's own examples under 4.1.3 Status Messages. Announce zero results just as clearly, because no results returned is on the same list.
Three details decide whether that region works.
- The count is the status message. The list is not. W3C says directly that a list of search results is not a status update, and only the brief message about the search is. Wrapping the results container in a live region announces every result and is the chatty failure the criterion warns about.
- The region has to be on the page before the message goes into it. Insert an empty container in your markup and write into it as a separate step. Create the container and its text in the same instant and you get silence. Our fix guide covers announcing a status with role=status, and the absence of all five mechanisms is the documented failure.
- The obligation lifts if the message takes focus or the page reloads. Those are the two conditions W3C attaches, in those words. So a full page reload does not engage this criterion at all, and neither does moving focus to a results heading. Pick one route. Doing both says it twice.
For search-as-you-type
Debounce the announcement, and know what you are fixing. A polite live region does not interrupt, it queues, so firing on every keystroke does not produce a stream of interruptions. It produces a backlog of stale counts, one per letter. Wait for a pause before you update the region, and check the politeness setting while you are there, because an assertive region really does cut across whatever the reader was listening to. The same rule covers the suggestion list, which the autocomplete guide has in full.
Where Focus Should Go
On a full reload the browser handles it. Focus starts at the top and the results heading tells the story, which is one reason a page load is the simpler build.
When results update in place, leave focus in the field and let the live region do the reporting. Moving focus to the results on every keystroke traps somebody mid-word, and moving it on submit is a legitimate alternative that then removes the need for the announcement.
Filters and Facets
Filters are where search accessibility usually collapses. Applying one re-renders the results silently and often moves the control that was just used.
- Group each set of filters in a
fieldsetwith alegend, so a checkbox is announced with the question it belongs to rather than on its own. This does more than name things. Where two facets both offer a Price range, the group is the only thing distinguishing the two identical labels. Do the grouping visually and in the code, and useoptgroupwhere the filter is aselect. Our fix guide covers grouping with fieldset and legend. - Announce the new result count through the same live region the search uses.
- Keep focus on the filter that was operated, rather than resetting it to the top of the page. The hard part nobody mentions is that after a re-render the element that had focus may no longer exist as the same node, so this needs deliberate handling rather than good intentions.
- Do not navigate on change. Reloading the page when somebody ticks a checkbox is a change of context under 3.2.2 On Input. Re-rendering the results in place usually is not, because a change of content only becomes a change of context when it moves focus, replaces the page, or significantly re-arranges it. There are two clean ways out. Offer an apply button, which works because activating a button is not changing the setting of a control, so it sits outside the criterion entirely. Or tell people what will happen before they touch the control, and note that the word before is the whole requirement. An explanation underneath the filter that the reader meets afterwards is not an advisory.
- Show applied filters as removable items, each with its own accessible name, so Remove filter, price under 50 rather than a row of identical crosses.
The Results Themselves
A list of results is a list, and marking it up as one gives the count away for free, so somebody hears twelve items rather than counting as they go.
Each result's link text should identify the page. Worth knowing which standard you are working to there. At Level AA the link's purpose may be determined from the link text together with its surrounding context, and the snippet underneath a result is that context, so a heading link normally passes without extra words. A link that identifies the page standing entirely alone is the Level AAA version. Aim for the second by all means, and do not file the first as a failure. Our link text checker will read every link on a page the way a screen reader lists them.