HTML
    forms

    When and how to use the HTML search element

    The HTML <search> element provides a semantic landmark for search and filtering interfaces without requiring ARIA roles. This article covers the exact criteria for when to use <search> versus a standard <form>, and how it impacts screen reader navigation.

    Editor: Paul RadfordJul 2, 20268 min read
    When and how to use the HTML search element

    When and how to use the HTML search element

    The <search> element is a semantic landmark that marks a section of a page as a search or filtering interface, giving screen reader users a navigable region without any extra ARIA attributes. Use it to wrap the form controls for site search or table filtering, and skip it for generic forms that don't retrieve or narrow down content.

    That one-line pitch undersells how long we waited for this. For years the only way to get a "search" landmark was role="search" bolted onto a <form> or a <div>. It worked, but it was one more attribute developers forgot to add. <search> bakes the semantics into the tag itself, and that matters more than it sounds like it should.

    What's the actual difference between <search> and <form role="search">

    Functionally, almost nothing, at least in terms of the accessibility tree. Both expose a landmark with the role "search." The difference is in intent and defaults.

    <form role="search"> requires you to remember the role every single time. Miss it, and you've got a form with no distinguishing semantics, just another region a screen reader user has to tab through blindly. <search> gives you the landmark by default, the same way <nav> gives you a navigation landmark without role="navigation".

    The bigger conceptual difference is that <search> is not a form. It's a sectioning element, similar to <section> or <aside>, and it can wrap a <form>, or it can wrap plain input controls with no <form> at all (think a client-side filter that just listens to input events and re-renders a list with JavaScript, no submission involved). <form role="search"> implies you're submitting something. <search> doesn't care whether you submit anything; it just says "this cluster of controls is for searching or filtering."

    That distinction is exactly why the WHATWG spec describes it as representing a "search or filtering" section of content, explicitly calling out filtering UI as a valid use case, not just classic search boxes.

    When should you use <search>?

    Use it when the controls inside genuinely let the user search or filter content on the page or site. That covers:

    • A site-wide search bar in the header, usually with a text input and a submit button.
    • An in-page filter UI for a data table, product listing, or search results page (checkboxes, selects, range sliders, whatever narrows the results down).
    • A "find in page" style widget for long documentation.

    Don't use it for:

    • Login forms, signup forms, checkout forms. None of these search or filter anything, so <search> would be semantically wrong even though nothing will technically break.
    • A newsletter signup box, even if it visually looks like a search bar.
    • Wrapping an entire page's content just because there's a search box somewhere on it. Scope <search> tightly around the actual search controls, not the results or the whole layout.

    One gotcha worth flagging: <search> is a sectioning element with landmark semantics, so nesting your entire main content inside it (because "well, the whole page is technically about search results") pollutes the landmark structure and confuses screen reader users doing landmark navigation. Keep it tight, wrap the inputs and maybe an inline suggestion list, not the results grid underneath.

    A complete site search bar example

    Here's a header search bar, the classic use case, using <search> instead of role="search".

    html
    1<header>
    2 <a href="/" class="logo">Acme Docs</a>
    3
    4 <!-- The search element wraps only the search controls,
    5 not the nav links or the rest of the header -->
    6 <search>
    7 <form action="/search" method="get" role="search" aria-label="Site search">
    8 <label for="site-search" class="visually-hidden">
    9 Search documentation
    10 </label>
    11 <input
    12 type="search"
    13 id="site-search"
    14 name="q"
    15 placeholder="Search docs..."
    16 autocomplete="off"
    17 />
    18 <button type="submit">Search</button>
    19 </form>
    20 </search>
    21
    22 <nav aria-label="Primary">
    23 <a href="/guides">Guides</a>
    24 <a href="/reference">Reference</a>
    25 </nav>
    26</header>

    Notice I kept role="search" on the inner <form> too. That's not a typo. Right now, some assistive technology and older browser/AT combinations don't yet map <search> to the search landmark reliably, so doubling up costs nothing and gives you a safety net. I'll get into support specifics in the next section.

    Also notice the <label> is still there, visually hidden or not. <search> gives you a landmark, but it does nothing for the input's accessible name. You still need a label, an aria-label, or an aria-labelledby on the form or input itself. This is a common mistake: developers assume the new element does more semantic heavy lifting than it actually does.

    A data table filtering example

    This is the use case that convinced me <search> earns its place beyond just "fancy header search bar." Filtering controls above a data table are a landmark navigation blind spot in almost every admin dashboard I've worked on. Wrapping them in <search> gives screen reader users a way to jump straight to the filters instead of tabbing through forty rows of table cells first.

    html
    1<section aria-labelledby="orders-heading">
    2 <h2 id="orders-heading">Orders</h2>
    3
    4 <!-- Filtering controls, not a submission form.
    5 No <form> needed here since JS handles filtering live. -->
    6 <search aria-label="Filter orders">
    7 <div class="filter-bar">
    8 <label for="filter-status">Status</label>
    9 <select id="filter-status" name="status">
    10 <option value="">All statuses</option>
    11 <option value="pending">Pending</option>
    12 <option value="shipped">Shipped</option>
    13 <option value="cancelled">Cancelled</option>
    14 </select>
    15
    16 <label for="filter-query">Search orders</label>
    17 <input
    18 type="search"
    19 id="filter-query"
    20 name="q"
    21 placeholder="Order number or customer name"
    22 />
    23
    24 <button type="button" id="clear-filters">Clear filters</button>
    25 </div>
    26 </search>
    27
    28 <table>
    29 <thead>
    30 <tr>
    31 <th scope="col">Order #</th>
    32 <th scope="col">Customer</th>
    33 <th scope="col">Status</th>
    34 <th scope="col">Total</th>
    35 </tr>
    36 </thead>
    37 <tbody id="orders-body">
    38 <!-- Rows rendered/filtered by JavaScript -->
    39 </tbody>
    40 </table>
    41</section>

    A quick script to back that up, filtering client-side without any form submission at all:

    js
    1// Filters visible rows based on the status select and text query.
    2// No form, no submit event, just live filtering, which is exactly
    3// the use case <search> was designed to cover.
    4const statusSelect = document.getElementById('filter-status');
    5const queryInput = document.getElementById('filter-query');
    6const clearBtn = document.getElementById('clear-filters');
    7const rows = () => document.querySelectorAll('#orders-body tr');
    8
    9function applyFilters() {
    10 const status = statusSelect.value.toLowerCase();
    11 const query = queryInput.value.trim().toLowerCase();
    12
    13 rows().forEach((row) => {
    14 const rowStatus = row.dataset.status?.toLowerCase() ?? '';
    15 const rowText = row.textContent.toLowerCase();
    16
    17 const statusMatch = !status || rowStatus === status;
    18 const queryMatch = !query || rowText.includes(query);
    19
    20 row.hidden = !(statusMatch && queryMatch);
    21 });
    22}
    23
    24statusSelect.addEventListener('change', applyFilters);
    25queryInput.addEventListener('input', applyFilters);
    26clearBtn.addEventListener('click', () => {
    27 statusSelect.value = '';
    28 queryInput.value = '';
    29 applyFilters();
    30});

    This is where <search> genuinely earns its keep, more than the header search bar honestly does. There's no <form> here, no submission, no role="search" fallback available because there's nothing to attach the role to besides a plain <div>, and previously most developers just used a <div class="filters"> with zero landmark semantics. Now there's a native element that says "this is the filtering UI" without you having to reach for ARIA at all.

    Screen reader support and what actually happens today

    This is the part where I have to be honest instead of optimistic. <search> shipped in Chrome 118 (October 2023) and Firefox 118, and Safari added support in Safari 17.4. That's the element itself rendering with the correct default role in the accessibility tree, which you can verify on MDN's browser compatibility table for the search element.

    Browser support having landed is not the same as screen readers reliably announcing "search landmark" when you land on it. VoiceOver on macOS and iOS handles it well in current Safari versions. NVDA and JAWS on Windows have caught up with recent releases paired with Chrome and Edge, but if a user is on an older screen reader version, or an older Firefox ESR build in a locked-down enterprise environment, they may get no landmark announcement at all, just silence where "search" should be.

    That's exactly why I doubled up <search> with role="search" in the site search example. Belt and suspenders costs you six characters of markup and protects users on the trailing edge of the support curve. For the filter example, there's no form to attach a role to, so I used aria-label on the <search> element itself to make sure it at least has a clear accessible name even in browsers that support the landmark but where the AT combo doesn't announce it well.

    One more nuance: <search> does not automatically make a screen reader announce every input inside it as a "search" field. It marks the container as a landmark region. Individual inputs still need their own accessible names via <label> or aria-label. Don't assume wrapping something in <search> absolves you of labeling individual controls, because it doesn't, and I've seen that assumption ship to production more than once.

    If you're building for an audience where you know a meaningful chunk of users are on Windows with older AT stacks, and you can't verify their exact versions, keep the role="search" fallback around for a while longer. It's not deprecated, it's not going away, and doubling up is explicitly fine per the spec since ARIA roles simply confirm what the native semantics already say.

    Key Takeaways

    • <search> shipped in Chrome 118, Firefox 118, and Safari 17.4, giving a default "search" landmark role with no ARIA needed, per MDN's documentation.
    • Unlike <form role="search">, <search> doesn't imply submission. It's valid for JavaScript-driven, client-side filtering UI with no <form> element at all.
    • Keep <search> scoped tightly around the actual controls (input, select, buttons), not around the results list or the entire page section.
    • Labeling still matters. <search> gives you a landmark, not an accessible name for the inputs inside it, so <label> and aria-label are still your job.
    • For production sites supporting older screen reader and browser combinations, pairing <search> with role="search" on the inner form costs nothing and covers users on lagging AT versions.

    Advertisement

    728 × 90 — Leaderboard — Google AdSense

    Related Articles