// 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.

native-html beginner wcag-2.2-aa

// 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

HTML — Search form with role=search
<!-- 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>
CSS — Focus, target size, and conditional clear button
.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;
}
JavaScript — Clear button, focus return, result count
// 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 + '"';
  });
}
Adding suggestions — when to upgrade to a combobox
// 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.
Focus return on clear is non-negotiable When the clear button hides itself (because the input is now empty), focus can land in the void. Always set focus back to the input in your click handler — not because the spec requires it, but because keyboard users get stranded otherwise.

// 05 · wcag 2.2 success criteria

WCAG 2.2 Success Criteria

This pattern satisfies the following WCAG 2.2 success criteria:

// 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

Placeholder as the only label <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.
Icon-only submit button with no 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.
No submit button at all — Enter only Mobile users, switch-device users, and voice-control users can be locked out. Always render a real submit button. Hide it visually only as a last resort, and even then verify your touch flow works without it.
Result count not announced (SC 4.1.3) The page re-renders, the count updates, and screen reader users hear nothing. Add a 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.
Clear button always visible, even when empty A persistent X button next to an empty field is confusing — what would it clear? Hide the clear button until the input has content. The CSS-only :not(:placeholder-shown) ~ .clear trick works for keyboard and pointer users without JavaScript.
Missing 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
Rule of thumb Use 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.