Eunomia v1.0.0-beta.2
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 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.
iconEunomiaIconName"search"Leading icon. Defaults to a search glyph instead of `EunomiaInput`'s unset default — still overridable via the `icon` attribute/property.
clearablebooleantrueWhether a clear button is displayed when the field has a value. Defaults to `true` instead of `EunomiaInput`'s `false`.
shortcutbooleanfalseEnables the Ctrl+K or Cmd+K global shortcut and its visual key hint badge
panelbooleanfalseOpens a modal command palette panel on shortcut or click instead of focusing inline
aria-label-panelstring'Search'The accessible name of the panel's dialog, used when label isn't set
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'Loading…'The accessible label announced by the loading indicator while loading is set
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'No results'Text displayed in the dropdown when no option matches, while suggestions is set
readonly-availablebooleanfalseWhether the field can switch between a static display and an editable one
default-valuestringThe default value applied when the field connects, and restored on form reset
placeholderstringThe placeholder shown when the field is empty
autocompletestring'off'The native autocomplete attribute forwarded to the field
minlengthnumberThe native minimum length constraint forwarded to the field
maxlengthnumberThe native maximum length constraint forwarded to the field
patternstringThe native pattern constraint forwarded to the field
minstringThe native minimum constraint forwarded to the field
maxstringThe native maximum constraint forwarded to the field
stepstringThe native step constraint forwarded to the field
empty-value-labelstring'No value set'Text displayed in place of the value when readonly and empty
confirm-labelstring'Save'The label of the confirm button in edit mode
cancel-labelstring'Cancel'The label of the cancel button in edit mode
aria-label-clearstring'Clear field'The accessible label of the clear button
aria-label-show-passwordstring'Show password'The accessible label of the show password button
aria-label-hide-passwordstring'Hide password'The accessible label of the hide password button

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
checkLocalValidatorsReads native HTML constraint validation from the underlying field element (`required`, `minlength`, `maxlength`, `pattern`, `min`, `max`, `type="email"`, etc.).
variant'outline' | 'fill' | 'underline''outline'The visual variant to apply to the field
labelstringThe title label of the field, rendered through the label component
instructionsstringInstructions displayed below the label
namestringThe field name
valuestringThe field value
disabledbooleanfalseWhether the field is disabled
readonlybooleanfalseWhether the field is read only
requiredbooleanfalseWhether the field is required
hideErrorbooleanfalseWhether the errors are hidden
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

Events

NameTypeDescription
eunchangeChangeEventFired on every value change
euncommitCommitEventFired when an edit is confirmed in readonlyAvailable mode

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

Basic

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

Custom icon

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

<eun-search
  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 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>

Variants

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

<eun-search name="search" placeholder="Fill" variant="fill"></eun-search>

With panel set, the collapsed trigger button mirrors the same variant, 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 name="search" placeholder="Fill" panel variant="fill"></eun-search>

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

Keyboard shortcut

<eun-search 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).

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 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[variant="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.

    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 (its text content, or its value attribute if explicitly set, same as a native <option>'s own .value property default) and closes the dropdown, and eunchange fires exactly like picking any other value would.

    <eun-search 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.

    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, 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 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 — 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 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.

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

      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[variant="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 ariaLabelPanel (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.

      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