Skip to content

ExpressiveCSS

Browse documentation

Search

A search bar, and the view it expands into.

View as Markdown

Add search-bar to a <search> element to make the bar. It holds a leading icon or icon button, an <input type="search">, and whatever trailing actions the query needs. The element is the landmark, so you do not need to add role="search".

<search class="search-bar" aria-label="Search">
  <span class="material-symbols" aria-hidden="true">search</span>
  <input type="search" aria-label="Search recipes" placeholder="Search recipes">
  <button type="button" class="icon-button" aria-label="Filters">
    <span class="material-symbols" aria-hidden="true">tune</span>
  </button>
</search>

The input has none of the text field chrome: no underline, floating label, or container of its own. The bar is the container, so put the input in bare, not inside a .field.

The bar sets its spacing from what is at each end, so you do not need to set anything. A bare glyph sits 16dp from the edge. An icon button sits 4dp from it, because the button already insets its own glyph by 12dp.

A leading action

When the bar opens a drawer or goes back, use an icon button for the leading glyph. The bar's leading space shrinks to 4dp on its own.

<search class="search-bar" aria-label="Search">
  <button type="button" class="icon-button" aria-label="Open navigation">
    <span class="material-symbols" aria-hidden="true">menu</span>
  </button>
  <input type="search" aria-label="Search mail" placeholder="Search mail">
  <button type="button" class="icon-button" aria-label="Account">
    <span class="material-symbols" aria-hidden="true">account_circle</span>
  </button>
</search>

An <img> in the bar is the account avatar M3 puts in that slot. It is 30dp and circular. Put it inside the button when it is tappable, so the 48dp target comes from the button.

Docked view

For the docked view, put a .search-view inside the bar. The view is positioned against the bar, which keeps its position: relative, so the view needs no coordinates of its own. Show and hide it with the hidden attribute.

The view is a plain element rather than a <dialog>, because dialog.show() moves focus into the dialog. That would take the caret out of the input the user is typing in.

The demo below starts open so the surface shows on the page. The sample under it has the hidden a real page would write. Focus the input to reopen it.

<search class="search-bar" aria-label="Search">
  <span class="material-symbols" aria-hidden="true">search</span>
  <input type="search" aria-label="Search fruit" placeholder="Search fruit"
         onfocus="document.getElementById('results').hidden = false">
  <button type="button" class="icon-button" aria-label="Close search"
          onclick="document.getElementById('results').hidden = true">
    <span class="material-symbols" aria-hidden="true">close</span>
  </button>

  <div class="search-view" id="results" hidden>
    <ul class="list">
      <li><a href="/apricot"><span class="material-symbols" aria-hidden="true">history</span><span>Apricot</span></a></li>
    </ul>
  </div>
</search>

Full-screen view

For the full-screen view, use a <dialog> and open it with showModal(). The browser handles Escape and a tap outside, so you only have to open it. Its header is another .search-bar with the same markup, minus the pill shape and the shadow.


<button type="button" onclick="document.getElementById('search-full').showModal()">
  Search
</button>

<dialog class="search-view full-screen" id="search-full" aria-label="Search">
  <search class="search-bar" aria-label="Search">
    <form method="dialog">
      <button type="submit" class="icon-button" aria-label="Back">
        <span class="material-symbols" aria-hidden="true">arrow_back</span>
      </button>
    </form>
    <input type="search" autofocus aria-label="Search fruit" placeholder="Search fruit">
  </search>
  <hr>
  <ul class="list">
    <li><a href="/apricot"><span class="material-symbols" aria-hidden="true">history</span><span>Apricot</span></a></li>
  </ul>
</dialog>

Put autofocus on the input. showModal() focuses the first focusable descendant, which is the back button, so without autofocus a keyboard user lands on Back and cannot type.

The M3 guideline is the full-screen view at Compact and the docked view from Medium up. Your app picks which one to show, because the stylesheet has no breakpoint that swaps one for the other. The two use different markup. The docked view is a plain element nested inside the bar, and the full-screen view is a top-level <dialog>, so render the one that fits the window size.

if (window.matchMedia('(width < 600px)').matches) {
  document.getElementById('search-full').showModal();
} else {
  document.getElementById('results').hidden = false;
}

Suggestions are Autocomplete

For suggestions, put autocomplete on the bar's input. You get the combobox, its listbox, and the arrow-key and aria-activedescendant handling from Autocomplete, which already implements and tests them. Search does not ship a suggestion list of its own.

<search class="search-bar" aria-label="Search">
  <span class="material-symbols" aria-hidden="true">search</span>
  <input type="search" class="autocomplete" aria-label="Search fruit" placeholder="Search fruit">
</search>
Expressive.Autocomplete.init(document.querySelector('.search-bar input'), {
  minLength: 0,
  data: [{ id: 'Apricot' }, { id: 'Blackcurrant' }, { id: 'Clementine' }]
});

Search and Autocomplete share nothing else, by design. A .search-view takes no composite role. Its contents are links and buttons that users reach with Tab. A second listbox here would announce the same behavior as Autocomplete's without the keyboard handling behind it. Use the view for recent searches, filters, and results. Use Autocomplete for a list the user arrows through.

Tokens

Override these on the bar or on the view.

Token Default
--md-comp-search-bar-container-color --md-sys-color-surface-container-high
--md-comp-search-bar-container-height 56px
--md-comp-search-bar-container-shape 9999px (full)
--md-comp-search-bar-leading-space 16px, or 4px beside an icon button
--md-comp-search-bar-trailing-space 16px, or 4px beside an icon button
--md-comp-search-bar-icon-label-space 16px
--md-comp-search-bar-icon-size 24px
--md-comp-search-bar-avatar-size 30px
--md-comp-search-view-container-color --md-sys-color-surface-container-high
--md-comp-search-view-container-shape 12px, 0 full-screen
--md-comp-search-view-header-height 56px, 72px full-screen
--md-comp-search-view-divider-color --md-sys-color-outline
--md-comp-search-view-bar-results-gap 2px
--md-comp-search-view-max-height 60vh, none full-screen

No matching pages

  • Source color

    The seed every generated ramp derives from. Pick one and browse the docs — the whole theme follows. Error does not: it is a fixed hue.

    Default source: Mist Iris.

    ExpressiveCSS presets: Lagoon, Harbor, Breeze, Cobalt, Iris.

    Bold

    Soft

    Mist

    Glow