Eunomia v1.1.0
AllAngularReactNext.jsJavaScriptVue
EunomiaSalmonForestVioletOceanGoldFireCustom…
🇬🇧 English🇫🇷 Français

Search

Search is a text field specialized for finding things, such as a site-wide search bar or a filter box above a list. It builds on the input field, adding a locked search type, an optional global keyboard shortcut, and an optional command-palette panel that opens the field in a modal instead of always showing it inline.

Dependencies

eun-icon · for the leading icon, shown by default eun-label · if it has a label eun-modal · for the command-palette panel, if panel is set eun-loader · for the spinning indicator, if loading is set
Overview API Examples Accessibility

When to use

Use search for the one field on a page whose job is finding something, such as a site-wide search bar or a filter box above a list or table. It builds on input, adding search-specific behavior such as a keyboard shortcut and an optional command-palette panel. For a plain text field with no search semantics, use input directly instead.

Install & usage

Pick a framework in the toolbar above and these snippets adapt.

npm install @eunomia/elements
import "@eunomia/elements/search.js";
<eun-search clearable name="search" placeholder="Search…" shortcut></eun-search>

Importing the file registers <eun-search> as a custom element, with no further setup needed. It works with any framework, or none, since it's a standard web component.

Alternatives

You want… Prefers
A plain text field with no search semantics eun-input

Guidance

  • Set shortcut only for a page's primary search: having several Ctrl+K listeners on one page fires all of them at once
  • Pair panel with a results-slotted list, kept in sync with the field's eunchange event
  • Call closePanel() from your result-selection handler once the user picks a result
  • Use suggestions for a fixed, known option set (recent searches, a short static list), and pair it with loading when the options themselves come from a server
  • Changing type: it's locked to "search" and setting it has no effect
  • Relying on min/max/step/readonly-available: inherited from eun-input but not meaningful for a live search field
  • Combining suggestions with panel: not supported, since suggestions is silently ignored while panel is set

Live testing

Properties

Search <eun-search>

Attributes

NameTypeDefaultDescription
type'text' | 'email' | 'number' | 'tel' | 'url' | 'search' | 'password'"search"Always `"search"` — enforced in `willUpdate`, since `EunomiaInput` declares `type` as a plain property (not an accessor), which TypeScript doesn't allow overriding as a getter/setter pair from a subclass. Never reflected, since it can't differ from its default.
iconEunomiaIconName"search"Leading icon. Defaults to a search glyph instead of `EunomiaInput`'s unset default — still overridable via the `icon` attribute/property.
shortcutbooleanfalseEnables the Ctrl+K or Cmd+K global shortcut and its visual key hint badge. A hidden search field ignores the shortcut, so several variants can have it and only the one shown answers
panelbooleanfalseOpens a modal command palette panel on shortcut or click instead of focusing inline
panel-labelstring—The accessible name of the panel's dialog, used when label isn't set. Defaults to the localized string for the current locale (English: "Search")
loadingbooleanfalseWhether a search request is currently in flight. Shows a spinning loading indicator in place of the clear button or shortcut hint on the inline field only. Ignored on the panel trigger and the panel's own field, which stay static, so reflect the loading state on the results slot instead
loading-labelstring—The accessible label announced by the loading indicator while loading is set. Defaults to the localized string for the current locale (English: "Loading…")
suggestionsbooleanfalseOpens a dropdown listbox showing light DOM option and optgroup children matching the typed value, highlighting the matched substring. Ignored while panel is set
no-results-labelstring—Text displayed in the dropdown when no option matches, while suggestions is set. Defaults to the localized string for the current locale (English: "No results")
filter'contains' | 'none''contains'How the suggestions are filtered against the typed value. Contains keeps the options whose label contains it, none shows every option as is, for results already filtered by a server
readonly-availablebooleanfalseWhether the field can switch between a static display and an editable one
default-valuestring—The default value applied when the field connects, and restored on form reset
placeholderstring—The placeholder shown when the field is empty
clearablebooleanfalseWhether a clear button is shown when the field has a value
autocompletestring'off'The native autocomplete attribute forwarded to the field
minlengthnumber—The native minimum length constraint forwarded to the field
maxlengthnumber—The native maximum length constraint forwarded to the field
patternstring—The native pattern constraint forwarded to the field
minstring—The native minimum constraint forwarded to the field
maxstring—The native maximum constraint forwarded to the field
stepstring—The native step constraint forwarded to the field
empty-value-labelstring—Text displayed in place of the value when readonly and empty. Defaults to the localized string for the current locale (English: "No value set")
confirm-labelstring—The label of the confirm button in edit mode. Defaults to the localized string for the current locale (English: "Save")
cancel-labelstring—The label of the cancel button in edit mode. Defaults to the localized string for the current locale (English: "Cancel")
clear-labelstring—The accessible label of the clear button. Defaults to the localized string for the current locale (English: "Clear field")
show-password-labelstring—The accessible label of the show password button. Defaults to the localized string for the current locale (English: "Show password")
hide-password-labelstring—The accessible label of the hide password button. Defaults to the localized string for the current locale (English: "Hide password")

Import the exact TypeScript type behind any property above, see Types.

Properties

JS-only — no matching HTML attribute, set these from a script or a template binding.

NameTypeDefaultDescription
checkLocalValidators—Reads native HTML constraint validation from the underlying field element (`required`, `minlength`, `maxlength`, `pattern`, `min`, `max`, `type="email"`, etc.).
appearance'outline' | 'flat' | 'underline''outline'How the field's surface is drawn: a bordered box, a muted background with no border, or a bottom border only
roundedbooleanfalseWhether the field is fully rounded, as a pill. A custom border radius still wins over it
size'xs' | 's' | 'm' | 'l' | 'xl''m'The field size. A field and a button of the same size share the same height
labelstring—The title label of the field, rendered through the label component
instructionsstring—Instructions displayed below the label
hideLabelbooleanfalseWhether the label is visually hidden. It still gives the field its accessible name, and the instructions stay visible
namestring—The field name
valuestring—The field value
disabledbooleanfalseWhether the field is disabled
readonlybooleanfalseWhether the field is read only
requiredbooleanfalseWhether the field is required
hideErrorbooleanfalseWhether the errors are hidden. The hint stays visible
invalidbooleanfalseShows the field as invalid without any message, for an error displayed elsewhere
errorstring—An error computed outside the field, by the form or by the server. While set, the field is invalid and shows this message right away
validatorsArray<Validators<string>>—The list of validation rules applied to the field value
readonlyAvailablebooleanfalseWhether the field can switch between a static display and an editable one

Slots

NameDescription
iconReplaces the leading icon entirely with any content, not just an icon element. Falls back to icon when empty. Applies to the real field, inline or inside the panel modal. The compact panel trigger button always shows the plain icon glyph, since it can't project the same slotted content a second time
resultsContent rendered below the field inside the panel, such as a result list. Only relevant when panel is set
triggerReplaces the panel's built-in trigger button, for example with an icon button. A click on it opens the panel, and focus returns to it on close. Give it its own accessible name. Only relevant when panel is set

Events

NameTypeDescription
eunchangeChangeEventFired on every value change, typed or picked from the suggestions
eunselectSelectEventFired when a suggestion is picked, with the option's value as key
euncommitCommitEventFired when an edit is confirmed in readonlyAvailable mode
euneditcancelEditCancelEventFired when an edit is cancelled in readonlyAvailable mode, after the previous value is restored

Every event above follows the same naming convention, covered in Events.

CSS custom properties

NameDescription
--field-listbox-max-heightSets the maximum height of the suggestions dropdown, past which it scrolls. Shared with the select component
--field-listbox-border-radiusSets the corner radius of the suggestions dropdown, independent of the field's own radius. Shared with the select component
--field-listbox-transition-durationSets the duration of the suggestions dropdown's open and close transition. Shared with the select component
--field-match-colorSets the color of the matched search substring, highlighted while suggestions is set. Shared with the select component
--field-match-backgroundSets the background of the matched search substring, none by default. Shared with the select component
--field-match-font-weightSets the font weight of the matched search substring. Shared with the select component
--field-match-text-decorationSets the text decoration, such as underline, of the matched search substring, none by default. Shared with the select component
--field-loader-colorSets the color of the spinner shown while loading, the icon color by default. Shared with the select component
--eun-viewport-inset-topReserves space, such as a page's sticky header height, that the suggestions dropdown keeps clear of at the top of the viewport. Set on the root or an ancestor rather than on the field itself
--eun-viewport-inset-bottomThe same as eun-viewport-inset-top, reserved at the bottom of the viewport, such as for a sticky footer
--field-backgroundSets the background color of the field, the surface color by default for the outline appearance
--field-border-colorSets the border color of the field
--field-border-color-hoverSets the border color on hover
--field-border-color-focusSets the border color on focus
--field-border-radiusSets the corner radius of the field
--field-paddingSets the padding of the field
--field-padding-blockSets the vertical padding, as one length applied to both sides. Ignored when --field-padding is set
--field-padding-inlineSets the horizontal padding, as one length applied to both sides. Ignored when --field-padding is set
--field-text-colorSets the text color of the field
--field-placeholder-colorSets the color of the placeholder
--field-icon-colorSets the color of the leading icon and action buttons
--field-error-colorSets the color of the error message
--field-hint-colorSets the color of the hint message
--field-focus-outline-colorSets the color of the focus outline
--field-edit-confirm-colorSets the color of the confirm button in readonly-available edit mode
--field-edit-cancel-colorSets the color of the cancel button in readonly-available edit mode
--field-font-sizeSets the font size of the field's value and placeholder
--field-line-heightSets the line height of the field's value and placeholder
--field-label-font-sizeSets the font size of the label
--field-message-font-sizeSets the font size of the hint, error messages and instructions

CSS shadow parts

NameDescription
triggerThe panel's built-in trigger button, shown while nothing is slotted in trigger

Basic

<eun-search clearable name="search" placeholder="Search…"></eun-search>

Display

Custom icon

icon (inherited from eun-input, defaulting here to a search glyph) still accepts any other eun-icon name.

<eun-search
  clearable
  name="search"
  icon="saved_search"
  placeholder="Search…"
></eun-search>

Need something other than a eun-icon, or a different icon library entirely? Slot your own content into icon instead ; it replaces the prop-driven icon entirely (the compact panel trigger button, when used, keeps showing the plain icon glyph, since it can't project the same slotted node a second time).

<eun-search clearable name="search" placeholder="Search…">
  <svg
    slot="icon"
    width="16"
    height="16"
    viewBox="0 0 24 24"
    fill="none"
    stroke="currentColor"
    stroke-width="2"
  >
    <circle cx="11" cy="11" r="7"></circle>
    <path d="m20 20-3.5-3.5"></path>
  </svg>
</eun-search>

Appearance

appearance (inherited from eun-input) selects the visual style: outline (default, bordered box), flat (muted background, no border), or underline (bottom border only), the same three appearances as every other text-like field in the library.

<eun-search
  clearable
  name="search"
  placeholder="Flat"
  appearance="flat"
></eun-search>

With panel set, the collapsed trigger button mirrors the same appearance, since it's a separate, hand-rolled element (not the inherited field itself), kept in sync with its own styling rather than looking like outline regardless of what's set:

<eun-search
  clearable
  name="search"
  placeholder="Flat"
  panel
  appearance="flat"
></eun-search>

Part of the shared vocabulary, alongside every other component that reuses it.

Keyboard shortcut

<eun-search clearable name="search" placeholder="Search…" shortcut></eun-search>

This demo leaves out shortcut so it doesn't compete with the documentation site's own header search, which already owns Ctrl+K/⌘K on every page (including this one). Press it now to try the real thing. On a page with a single search field, add shortcut as shown above.

Pressing Ctrl+K (or ⌘K on Mac) anywhere on the page focuses the field. The key-hint badge inside the field is purely decorative (aria-hidden), while the field itself carries aria-keyshortcuts="Control+K Meta+K" while shortcut is set (see the Accessibility tab).

A hidden search field ignores the shortcut, whether it is hidden with display: none itself or through one of its ancestors. A responsive layout can therefore render a full field and a compact one, both with shortcut, and only the one shown at the current breakpoint answers Ctrl+K.

The badge's displayed label, ⌘K versus Ctrl+K, depends on the operating system the browser itself reports, not on real device detection: it reads navigator.userAgentData.platform (falling back to the older, deprecated navigator.platform on browsers without User-Agent Client Hints support) and shows ⌘K for anything reporting a Mac-family platform, Ctrl+K otherwise. Both keyboard shortcuts (Control+K and Meta+K) are always listened for regardless of the detected platform, since only the visual badge adapts. A Mac keyboard connected to a non-Mac machine (or a browser/OS spoofing its reported platform) still triggers the shortcut correctly even if the badge itself shows the "wrong" one.

Modal panel

    This demo also leaves out shortcut, for the same reason as the "Keyboard shortcut" example above. See its note, and the Guidance section on the Overview tab.

    <eun-search clearable name="search" placeholder="Search…" shortcut panel>
      <ul slot="results" id="results"></ul>
    </eun-search>
    const search = document.querySelector("eun-search");
    const results = document.querySelector("#results");
    
    search.addEventListener("eunchange", () => {
      // Re-render #results from search.value — the field inside the panel
      // shares the same `value`/`eunchange` as the inline field.
    });
    
    // From a result's click handler, once selected:
    search.closePanel();

    With panel set, the visible field becomes a trigger button, and clicking it (or Ctrl+K) opens a eun-modal[kind="search"] containing the real, editable field plus the results slot: a flush, full-width layout (no padding, no border, no close button) with the field's own bottom border separating it from the result list below. See eun-modal's own "Search and command palette" example for the layout on its own. The field carries an Esc badge instead of the collapsed trigger's Ctrl+K/⌘K one, since Escape is how the panel closes once open. Typing in the panel still updates value and fires eunchange exactly like the inline field would, but the trigger button itself stays static: it always shows placeholder, never the current value, since it's purely an entry point into the panel rather than a second display of what's typed there.

    Custom trigger

    Slot your own element in trigger to replace the built-in trigger, for example an icon button in a top bar. A click on it opens the panel, and closing the panel returns focus to it. The built-in trigger is then no longer rendered, focusable or announced. Give the slotted element its own accessible name. To restyle the built-in trigger instead, target its trigger part.

      <eun-search clearable placeholder="Search…" panel>
        <eun-button slot="trigger" icon="search" aria-label="Search"></eun-button>
        <ul slot="results" id="results"></ul>
      </eun-search>
      eun-search::part(trigger) {
        border-radius: 999px;
      }

      Suggestions

      Set suggestions to open a dropdown listbox of matching option children as the user types, highlighting the matched substring: light DOM option (and optgroup) children, parsed and filtered exactly like eun-select's own. Picking a suggestion sets value to its label and closes the dropdown. eunchange fires with that label, like a keystroke would, then eunselect fires with the option's value attribute as key, so a pick can be told apart from typing, and two options sharing a label stay distinguishable.

      <eun-search clearable name="search" placeholder="Search…" suggestions>
        <option>Getting started</option>
        <option>Installation</option>
        <option>Components</option>
        <option>Theming</option>
        <option>Accessibility</option>
      </eun-search>

      Not supported together with panel, since a collapsed trigger button plus two independent top-layer light-dismiss surfaces (the modal's own and the listbox popover's) isn't a combination this component implements. suggestions is silently ignored while panel is set.

      Remote results

      When a server returns the results, set filter="none" so the field shows the options as they are instead of filtering them again on their label, which would hide a result the server matched on another field. Replace the option children whenever a response comes back, and the dropdown follows. Listen to eunselect to open the picked result.

      <eun-search
        clearable
        name="search"
        placeholder="Search…"
        suggestions
        filter="none"
      ></eun-search>
      import { debounce } from "@eunomia/elements";
      
      const search = document.querySelector("eun-search");
      
      const runSearch = debounce(async (query: string) => {
        search.loading = true;
        try {
          const results = await fetchResults(query); // your own API call
          search.replaceChildren(
            ...results.map((result) => new Option(result.title, result.id)),
          );
        } finally {
          search.loading = false;
        }
      }, 300);
      
      search.addEventListener("input", () => runSearch(search.value));
      search.addEventListener("eunselect", (event) => openResult(event.key));

      Dropdown positioning, sizing, and theming

      Same mechanic as eun-select's own dropdown, sharing its CSS custom properties. AnchorPositionMixin keeps the panel positioned against the field on every scroll/resize, flipping above it when there isn't room below, via CSS Anchor Positioning natively where supported and a JS fallback everywhere else. See eun-select's own "Dropdown panel: rendering, positioning, and sizing" section (Examples tab) for the full explanation, including --eun-viewport-inset-top/-bottom for page chrome like a sticky header. --field-listbox-max-height (default 260px) caps how tall it gets before scrolling, --field-listbox-border-radius rounds its corners without following --field-border-radius, while --field-match-color/-background/-font-weight/ -text-decoration theme the highlighted substring. Set any of these once and both eun-search and eun-select pick it up.

      Loading

      Set loading while a search request is in flight, and a spinning eun-loader replaces the clear button/shortcut badge, without disabling the field, since a live search commonly keeps accepting input while the request for the previous keystroke is still in flight.

      <eun-search clearable name="search" placeholder="Search…" loading></eun-search>
      import "@eunomia/elements/loader.js";

      eun-loader is used internally, but its module must still be imported alongside eun-search for the spinner to render, exactly like eun-icon for the leading icon above.

      With panel set, loading is ignored on both the collapsed trigger button and the panel's own field, which both stay static entry points regardless of a request in flight. Reflect the loading state on the results slot instead, as in the "Debouncing a real request" example below.

      Debouncing a real request

      eun-search has no idea what "the request" behind loading actually is, since that's entirely the consumer's own code, wired to eunchange. Debouncing it so a fast typist doesn't fire one request per keystroke is that same consumer's responsibility too, so @eunomia/elements exports a small debounce helper for exactly this, meaning nobody has to reach for a whole utility library (like Lodash) just for this one function, or reimplement it by hand.

      This example puts the two together inside panel's results slot: typing debounces, loading shows while the (simulated) request is in flight, with a eun-loader replacing the result rows entirely instead of leaving stale or empty content on screen, then the real rows replace it once the response is back. This is exactly why loading stays invisible on the trigger and the panel's own field in panel mode: the list is where the in-flight state actually belongs, not duplicated onto the field above it.

        <eun-search clearable name="search" placeholder="Search…" panel>
          <ul slot="results" id="results"></ul>
        </eun-search>
        import { debounce } from "@eunomia/elements";
        
        const search = document.querySelector("eun-search");
        const results = document.querySelector("#results");
        
        const runSearch = debounce(async (query: string) => {
          search.loading = true;
          try {
            const matches = await fetchResults(query); // your own API call
            renderResults(matches); // your own rendering, e.g. a eun-loader row while empty
          } finally {
            search.loading = false;
          }
        }, 300);
        
        search.addEventListener("eunchange", () => runSearch(search.value));

        300ms is a reasonable starting point for a debounce delay on a live search, long enough to skip firing on every keystroke of a fast typist, yet short enough to still feel immediate. Tune it against your own API's actual latency.

        Server-side rendering

        The field is drawn inside the search's shadow root, so a server-rendered search takes no space until the element upgrades, then pushes aside what follows it. Link field-ssr.css in the page's <head> to draw it from the first paint, search icon included:

        <link
          rel="stylesheet"
          href="node_modules/@eunomia/elements/dist/ssr/field-ssr.css"
        />

        It's the same stylesheet as eun-input's, reading the same attributes. The suggestion options stay hidden until the upgrade. The panel trigger isn't drawn before the upgrade.

        Custom colors

        The custom properties that change colors, borders, shadows and corners, at a glance. The API tab lists every property of the component.

        Variable Sets
        --field-match-color Sets the color of the matched search substring, highlighted while suggestions is set. Shared with the select component
        --field-match-background Sets the background of the matched search substring, none by default. Shared with the select component
        --field-background Sets the background color of the field, the surface color by default for the outline appearance
        --field-border-color Sets the border color of the field
        --field-border-color-hover Sets the border color on hover
        --field-border-color-focus Sets the border color on focus
        --field-border-radius Sets the corner radius of the field
        --field-text-color Sets the text color of the field
        --field-placeholder-color Sets the color of the placeholder
        --field-icon-color Sets the color of the leading icon and action buttons
        --field-loader-color Sets the color of the spinner shown while loading
        --field-error-color Sets the color of the error message
        --field-hint-color Sets the color of the hint message
        --field-focus-outline-color Sets the color of the focus outline
        --field-edit-confirm-color Sets the color of the confirm button in readonly-available edit mode
        --field-edit-cancel-color Sets the color of the cancel button in readonly-available edit mode
        --field-listbox-border-radius Sets the corner radius of the suggestions dropdown, independent of the field's own radius. Shared with the select component

        Keyboard interactions

        Key Action
        Ctrl+K / ⌘K Focuses the field (or opens the panel, if set), only while shortcut is set, from anywhere on the page
        Tab / Shift+Tab Moves focus in/out of the field (inline), or the trigger button / panel content (panel mode)
        Enter, in the field Picks the highlighted suggestion, otherwise submits the closest ancestor <form>, as a native text field does
        Enter / Space Activates the trigger button, opening the panel (panel mode only, native button semantics)
        Escape Closes the panel (native to eun-modal, see its own Keyboard interactions)

        While suggestions is set (inline only):

        Key Action
        ArrowDown / ArrowUp Opens the suggestions if closed and highlights the next/previous one, without leaving the field
        Enter Picks the highlighted suggestion, otherwise submits the form
        Escape Closes the suggestions (native Popover API light-dismiss)

        Semantics and screen readers

        Aria attributes

        The key-hint badge (⌘K/Ctrl+K/Esc) is purely decorative and marked aria-hidden="true" in every context it appears. The actual describable shortcut lives on whichever element it really activates:

        • Inline (panel not set): the field itself carries aria-keyshortcuts="Control+K Meta+K" while shortcut is set.
        • Panel mode: the trigger button carries aria-keyshortcuts instead, since the real field sits inside a closed popover until opened, and content inside a closed popover is removed from the accessibility tree entirely, so describing the shortcut there wouldn't be discoverable. The trigger button also carries aria-haspopup="dialog".

        The panel's dialog (eun-modal[kind="search"]) renders no visible heading, so it wouldn't otherwise have an accessible name. eun-search sets a plain aria-label on it directly: label when set (naming the dialog the same as the field), falling back to panelLabel (default "Search") otherwise. See eun-modal's own Accessibility tab for the dialog's other semantics (role="dialog", focus handling, Escape), which also covers focus management in more depth, including this component's own case: the panel's field gets autofocus so keyboard focus lands there immediately on open, and closing it (however that happens) returns focus to wherever it was before, either the trigger button if that's what opened it, or elsewhere on the page if a global Ctrl+K/⌘K press did.

        Suggestions are a combobox

        While suggestions is set (inline only), the field follows the WAI-ARIA APG combobox pattern with list autocomplete, like a searchable eun-select:

        • role="combobox", aria-autocomplete="list", aria-controls="listbox" and aria-expanded="true"\|"false" on the field
        • aria-activedescendant on the field, set to the highlighted suggestion while one is highlighted, so a screen reader announces it without moving the DOM focus off the field
        • role="listbox" on the dropdown, named from the field's label, with role="option" / aria-selected on each suggestion, and the suggestions of each optgroup wrapped in a role="group" named by its label

        Results are not a combobox/listbox

        The results slot is fully consumer-defined content (any markup, a list, a grid, grouped sections), and eun-search doesn't know its shape, so it doesn't implement the ARIA combobox/listbox pattern (role="combobox", aria-expanded, aria-controls, aria-activedescendant, arrow-key roving focus between results). Tab still reaches every result normally, same as any other focusable content, but there's no arrow-key navigation between them out of the box. If you need that, implement roving tabindex / aria-activedescendant yourself on your own slotted markup, following the ARIA combobox pattern.

        Reference links

        WAI-ARIA Dialog (Modal) Pattern
        WAI-ARIA Combobox Pattern
        MDN: aria-keyshortcuts