// pattern
Accessible Search Field Pattern
Search is one of the most-used UI elements on any content site. A small accessibility miss — a missing label, a hidden submit button, an icon with no name — quietly breaks search for keyboard, screen reader, and voice users. Here's a search field that works for everyone.
// 01 · live demo
Live Demo
Header search — visually-hidden label, icon submit, clear button (appears when there's text).
Inline search with announced result count:
Submit the form (Enter or click). The result count updates in a role="status" live region — screen readers announce it without moving focus.
Search with recent searches (focus the input):
Each suggestion is a real <button> — focusable, activatable with Enter or Space. For full autocomplete with arrow-key navigation, upgrade to a combobox.
Bad example 1 — placeholder as the only label:
No label at all. The placeholder disappears the moment a user types a character. Voice-control users can't say "click search" — there's no name to target. The submit icon button has no aria-label, so screen readers announce only "button". And there's no role="search" landmark.
Bad example 2 — icon-only submit with no accessible name:
The input has a label, but the submit button is a magnifying glass <svg> with no text and no aria-label. Screen readers announce "button", not "submit search". This is the single most common search-field bug in the wild.
// 02 · the code
The Code
<!-- role="search" turns the form into a landmark.
Use it once per page on the primary search. -->
<form role="search" action="/search" method="get" class="search">
<!-- A real label, even if visually hidden.
Never use placeholder as the only label. -->
<label for="site-search" class="visually-hidden">
Search the site
</label>
<input
type="search"
id="site-search"
name="q"
class="search__input"
placeholder="Search…"
autocomplete="off">
<!-- Clear button: hidden until the input has content.
aria-label gives an icon-only button a name. -->
<button type="button"
class="search__clear"
aria-label="Clear search"
data-clear-for="site-search">
<svg aria-hidden="true"><!-- × icon --></svg>
</button>
<!-- Visible submit button. Even if Enter submits,
motor / touch users may need a real button. -->
<button type="submit"
class="search__submit"
aria-label="Submit search">
<svg aria-hidden="true"><!-- magnifier --></svg>
</button>
</form>
<!-- Live region for announced result count.
Stays in the DOM at all times; JS writes into it. -->
<div role="status" class="search__results"></div>
.search {
display: flex;
align-items: center;
gap: 0.25rem;
padding: 0.25rem;
border: 1px solid #d1d5db;
border-radius: 0.25rem;
background: #fff;
}
/* SC 2.4.7 / 2.4.13: visible focus on the
whole search bar when the input is focused. */
.search:focus-within {
border-color: #5b2a86;
box-shadow: 0 0 0 2px rgba(26, 86, 219, 0.2);
}
.search__input {
flex: 1;
min-width: 0;
padding: 0.25rem 0.5rem;
border: none;
background: transparent;
font-size: 1rem;
outline: none;
}
/* Hide the browser's native clear button on
type="search" — we provide our own with a
guaranteed accessible name. */
.search__input::-webkit-search-cancel-button {
-webkit-appearance: none;
appearance: none;
}
/* SC 2.5.8: 24x24 minimum target size. */
.search__clear,
.search__submit {
display: inline-flex;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
min-width: 24px;
min-height: 24px;
padding: 0;
border: none;
border-radius: 0.25rem;
cursor: pointer;
}
.search__clear:focus-visible,
.search__submit:focus-visible {
outline: 2px solid #5b2a86;
outline-offset: 2px;
}
/* Show the clear button only when the input
has content. The placeholder-shown sibling
trick avoids any JS for show/hide. */
.search__clear { display: none; }
.search__input:not(:placeholder-shown)
~ .search__clear {
display: inline-flex;
}
// Clear button: empty the input, return focus.
document.querySelectorAll('[data-clear-for]').forEach(function (btn) {
btn.addEventListener('click', function () {
var input = document.getElementById(btn.getAttribute('data-clear-for'));
if (!input) return;
input.value = '';
// Focus return is critical: without it, keyboard
// users get dropped to the start of the page on
// some browsers when the now-hidden button vanishes.
input.focus();
// Fire 'input' so any listeners (filters, suggestions)
// know the value changed.
input.dispatchEvent(new Event('input', { bubbles: true }));
});
});
// Announce result count after a search.
function announceResults(count, query) {
var region = document.querySelector('[role="status"].search__results');
if (!region) return;
// Clear first so identical text re-announces.
region.textContent = '';
// requestAnimationFrame helps screen readers
// notice the change as a fresh insertion.
requestAnimationFrame(function () {
region.textContent =
count + ' result' + (count === 1 ? '' : 's') +
' for "' + query + '"';
});
}
// A "recent searches" list rendered as plain <button>
// elements is fine if users only click them. They are
// reachable with Tab and operable with Enter / Space.
//
// If you need any of the following, upgrade to a
// combobox (see /patterns/combobox/):
//
// - Arrow-key navigation through suggestions
// - Live filtering as the user types
// - "Selected" announcement of the active suggestion
// - aria-activedescendant or aria-selected semantics
//
// A combobox uses role="combobox" on the input and
// role="listbox" on the suggestions, plus
// aria-expanded, aria-controls, and aria-activedescendant
// to wire focus management.
// 03 · why these decisions
Why These Decisions
Why role="search"?
A <form role="search"> becomes a landmark in the accessibility tree. Screen reader users can press a single shortcut (D in NVDA, VO + U in VoiceOver) to jump to the search region, the same way sighted users scan for a magnifying glass icon. Without it, the form is just "form" — indistinguishable from a contact form or login. Use role="search" exactly once per page on the primary search.
Why a real <label>, not a placeholder?
Placeholders fail accessibility on every axis:
- They disappear when the user starts typing — short-term memory and cognitive load problems.
- They have weak default contrast — most browsers render them at ~50% opacity, failing WCAG color contrast.
- They are confusing — users mistake them for pre-filled values and skip the field.
- They are not announced reliably as the field name across all screen readers.
SC 1.3.1 (Info and Relationships) and SC 3.3.2 (Labels or Instructions) both require a real label. The placeholder is fine as a hint — never as the label.
When is class="visually-hidden" acceptable?
If the field's purpose is unmistakable from surrounding context (a magnifier icon, a submit button labeled "Search", placement in the page header), a visually-hidden label is acceptable. The label still exists in the accessibility tree for screen readers and voice control. If the search field could be confused with another input — say, a header with both a search and an email signup — use a visible label.
Why type="search" over type="text"?
Three benefits, one caveat:
- Mobile keyboards show a "Search" key instead of "return".
- Screen readers announce "search edit" instead of "edit", giving a clearer mental model.
- The browser provides a native clear button in some browsers (WebKit, Edge).
The caveat: the native clear button is inconsistent (Firefox doesn't render it; styling it is awkward). Hide the native button with ::-webkit-search-cancel-button { appearance: none; } and provide your own — that way every user gets the same control with a guaranteed accessible name.
Why a visible submit button?
"Just press Enter" excludes a lot of people:
- Touch users on a phone with no physical keyboard — they expect a button to tap.
- Switch and motor users who use a single-action input device may not have an "Enter" gesture.
- Voice users say "click submit" or "click search" — there has to be something to click.
- Discoverability — new visitors look for a button to confirm the field is interactive.
The submit button must meet the 24×24 minimum (SC 2.5.8) and have an accessible name. If it's icon-only, give it aria-label="Submit search".
Why does the clear button need aria-label?
An icon-only <button> with just an SVG inside has no accessible name. Screen readers announce "button" — useless. aria-label="Clear search" gives it a name; voice users can say "click clear search"; SC 4.1.2 (Name, Role, Value) is satisfied. Add aria-hidden="true" to the SVG so its content isn't read on top of the label.
Why announce result counts via a live region?
When a user submits a search and the page re-renders the results list below the field, sighted users see the new count. Screen reader users — especially those using browse mode — may not realize the page changed at all. Writing the count into a role="status" region (which is implicitly aria-live="polite") speaks the result count without moving focus or interrupting. SC 4.1.3 (Status Messages) is the criterion.
When should I upgrade to a combobox?
If you're showing live autocomplete suggestions with arrow-key navigation, use the combobox pattern. A search field with a static list of recent searches (like the demo above) does not need combobox semantics — each suggestion is just a button. The line: if the input and the suggestion list need to share keyboard focus management (arrow keys move highlight, Enter selects the highlighted item), it's a combobox.
// 04 · keyboard interaction
Keyboard Interaction
| Key | Action |
|---|---|
| Tab | Moves focus to the search input. |
| Type | Enters the query. The clear button appears once the input has content. |
| Enter | Submits the form. |
| Tab (from input) | Moves focus to the clear button (when visible), then to the submit button. |
| Enter or Space on clear | Empties the input and returns focus to the input. |
| Enter or Space on submit | Submits the form. |
| Esc (optional) | Some implementations clear the input. Browsers expose this natively for type="search" in some platforms — don't rely on it as the only way to clear. |
// 05 · wcag 2.2 success criteria
WCAG 2.2 Success Criteria
This pattern satisfies the following WCAG 2.2 success criteria:
-
1.3.1 Info and Relationships
Level A
— The
<label>is programmatically associated with the input viafor/id. - 2.4.6 Headings and Labels Level AA — Labels describe the field's purpose ("Search the site"), not just its visual style.
- 2.4.7 Focus Visible Level AA — Input, clear, and submit all show a visible focus indicator.
- 2.5.8 Target Size (Minimum) Level AA — Submit and clear buttons meet the 24×24px minimum (new in WCAG 2.2).
- 3.3.2 Labels or Instructions Level A — A real label is provided, not just placeholder text.
-
4.1.2 Name, Role, Value
Level A
— Icon-only submit and clear buttons have
aria-labelnames. -
4.1.3 Status Messages
Level AA
— Result counts are announced via a
role="status"live region without moving focus.
// 06 · screen reader behavior
Screen Reader Behavior
Entering the search region
Because the form has role="search", when a screen reader user enters it the region is announced — typically "search region" or "search landmark" depending on the screen reader. Users navigating by landmark (NVDA's D, VoiceOver's rotor) can jump straight here.
The empty input
Focusing the empty input announces something like "Search the site, search edit, blank" — the label, the input type ("search edit" because of type="search"), and the empty state.
Input with a value
If the user types or selects a recent search, focus on the populated input announces "Search the site, search edit, accessibility" — label, type, current value.
After submit, with the result-count region
The user submits, the page (or a fragment) re-renders, and the script writes "12 results for accessibility" into the role="status" region. Screen readers wait for the current speech to finish, then announce the new text. Focus stays on the input or moves to the results heading — your choice, but never both.
The recent-searches list
The list has aria-label="Recent searches". Screen readers announce "Recent searches, list, 5 items" on entry, then read each item as "button, aria live regions", etc. Each suggestion is a real button so it's reachable with Tab and operable with Enter or Space.
// 07 · common mistakes
Common Mistakes
<input placeholder="Search…"> with no <label> fails SC 1.3.1, SC 3.3.2, and color contrast. Placeholders disappear on input, are easily missed, and aren't reliable accessible names. Always pair the input with a real label — visually hidden if you must, but always present in the markup.
aria-label
<button><svg/></button> announces as just "button". Voice users can't say "click submit search" because there's no name to match. Add aria-label="Submit search" and keep the SVG aria-hidden="true" so the label isn't doubled.
role="status" region in the DOM at load and write the result string into it on each submit. Don't move focus to the results unless the user expects it.
:not(:placeholder-shown) ~ .clear trick works for keyboard and pointer users without JavaScript.
role="search" landmark
Without it, your search form is indistinguishable from any other form in landmark navigation. Screen reader users can't jump directly to search and have to walk through the page. Add role="search" to the primary site search and use it exactly once per page.
// 08 · native input types compared
Native Input Types Compared
type="search" versus type="text" for a search field.
| Aspect | type="text" |
type="search" |
|---|---|---|
| Browser-native clear button | None | Yes in WebKit / Edge, none in Firefox — inconsistent |
| Mobile keyboard hint | "return" or "go" key | Dedicated "Search" key |
| Semantic meaning | Generic text input | Conveys "this field is for search" |
| Screen reader announcement | "edit" | "search edit" (gives users a clearer mental model) |
| Esc key clears | No | Yes in some browsers (don't rely on it) |
| Recommendation | Use only for non-search inputs | Prefer for any search field — provide your own clear button for consistency |
type="search" for the semantic and mobile-keyboard wins, hide the inconsistent native clear button with ::-webkit-search-cancel-button { appearance: none; }, and ship your own clear button with a guaranteed accessible name.