Search Panel
A slide-in search overlay. Open it, type a query, and pick a result. It is form-associated — the live query is mirrored into the owning form — and also dispatches bubbling search and result-select events. Esc or the backdrop closes it.
Preview
Set el.results in a script, then open with el.open = true.
<button id="open-right">Open search</button>
<esa-search-panel id="sp-right" position="right"></esa-search-panel> import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import '@esa/ecology/esa-search-panel';
@Component({
selector: 'app-search-panel-demo',
standalone: true,
schemas: [CUSTOM_ELEMENTS_SCHEMA],
template: `
<button id="open-right">Open search</button>
<esa-search-panel id="sp-right" position="right"></esa-search-panel>
`,
})
export class SearchPanelDemo {} Positions
The panel slides in from the right (default) or the left.
<esa-search-panel position="right"></esa-search-panel>
<esa-search-panel position="left"></esa-search-panel> import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import '@esa/ecology/esa-search-panel';
@Component({
selector: 'app-search-panel-demo',
standalone: true,
schemas: [CUSTOM_ELEMENTS_SCHEMA],
template: `
<esa-search-panel position="right"></esa-search-panel>
<esa-search-panel position="left"></esa-search-panel>
`,
})
export class SearchPanelDemo {} States
Set loading to show a searching message while results are fetched.
<esa-search-panel position="right" loading></esa-search-panel> import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import '@esa/ecology/esa-search-panel';
@Component({
selector: 'app-search-panel-demo',
standalone: true,
schemas: [CUSTOM_ELEMENTS_SCHEMA],
template: `
<esa-search-panel position="right" loading></esa-search-panel>
`,
})
export class SearchPanelDemo {} API
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | false | Reflected attribute that drives visibility. Toggle by setting the property. |
placeholder | string | 'Search...' | Input placeholder text. |
results | EsaSearchResult[] | [] | Results to render, set as a property. Each: { id, title, subtitle?, icon?, category? }. Same-category items are grouped. |
loading | boolean | false | Reflected; shows a loading message instead of results. |
position | 'right' | 'left' | 'right' | Side the panel slides in from. |
name | string | undefined | — | Form field name — the key this control submits under. |
Events
| Event | Type | Default | Description |
|---|---|---|---|
search | CustomEvent<{ value: string }> | — | Fired on each input change with the current query. |
result-select | CustomEvent<EsaSearchResult> | — | Fired when a result is chosen. |
open-change | CustomEvent<{ open: boolean }> | — | Fired when the panel closes (Esc or backdrop). |
Binding these events from Angular
Every one of these is a CustomEvent, so the payload is on
$event.detail — $event.target.value reads empty, with no
error. The names are lowercase and Angular passes them to
addEventListener verbatim, so camel-casing one means it never fires.
(open-change)="onOpenChange($event.detail)"(result-select)="onResultSelect($event.detail)"(search)="onSearch($event.detail)"
The live query also participates in the enclosing <form> via ElementInternals.
Accessibility
- The panel is
role="search"; the close button carries anaria-label. - The input is focused automatically when the panel opens.
Escapeand backdrop click both close the panel and fireopen-change.
Theming surface
Every public token this component reads, extracted from its source at build time,
split by blast radius: the hooks that belong to this component,
then the shared and system tokens it consumes alongside everything else. The
Resolves to column walks each token's real lineage —
component → semantic → primitive → raw value — so you
can see exactly where re-pointing it lands. A spoke re-skins by overriding these in
its theme-<slug>.css; component internals are never edited. No
hook for what you need? File it with /request-lego.
Type roles it names
This component does not set a size, weight, face or tracking of its own — it names a composite, and the composite carries all five. A composite is a class rather than a token, so none of it appears in the tables below; re-theme text by re-pointing the token family, not by editing the component.
| Role | Re-point | Reach |
|---|---|---|
.typography-body-sm |
| +20 more componentsesa-alert-box, esa-breadcrumbs, esa-button-toggle, esa-card, esa-checkbox-group, esa-combobox, esa-command-palette, esa-date-picker, esa-empty-state, esa-file-upload, esa-form-field, esa-input-tag, esa-link-column, esa-loading-overlay, esa-progress-bar, esa-radio-group, esa-select, esa-stat, esa-text-field, esa-textarea |
.typography-body-xs |
| +14 more componentsesa-breadcrumbs, esa-checkbox, esa-checkbox-group, esa-combobox, esa-command-palette, esa-empty-state, esa-entity-search, esa-field-error, esa-header-nav, esa-input-tag, esa-progress-bar, esa-radio-group, esa-select, esa-textarea |
.typography-eyebrow-md |
| +3 more componentsesa-command-palette, esa-entity-search, esa-sidebar-nav |
.typography-label-sm |
| +3 more componentsesa-breadcrumbs, esa-progress-bar, esa-sidebar-nav |
.typography-microcopy-md-subtle |
| +7 more componentsesa-combobox, esa-date-picker, esa-filter-dropdown, esa-input-tag, esa-pagination, esa-select, esa-text-field |
Wired to this component
No other component reads these — re-point them and esa-search-panel is the
only thing that moves.
| Token | Tier | Resolves to |
|---|---|---|
--search-panel-width | component | 400px |
Shared tokens it reads
Owned by a family or by the semantic layer. Still overridable — but scope the
override (esa-foo { --form-border-color: … }) unless you mean to move
everything that reads it. Reach says what else that is.
| Token | Reach | Resolves to |
|---|---|---|
--focus-ring-color | component focus ring group · +34 more | #3e9b4f via --color-border-default-focus → --color-grass-10 |
--focus-ring-offset | component focus ring group · +31 more | 2px |
--focus-ring-width | component focus ring group · +35 more | 2px via --border-width-focus → --border-width-200 |
--animation-overlay-enter | semantic every component | 250ms via --duration-250 |
--border-width-default | semantic every component | 1px via --border-width-100 |
--color-background-elevation-floating | semantic every component | #fcfcfc via --color-gray-1 |
--color-background-elevation-sunken | semantic every component | #f0f0f0 via --color-gray-3 |
--color-background-overlay-backdrop | semantic every component | rgba(0, 0, 0, 0.5) via --color-black-a-7 |
--color-border-default-subtle | semantic every component | #d9d9d9 via --color-gray-6 |
--color-content-default | semantic every component | #202020 via --color-gray-12 |
--color-content-default-secondary | semantic every component | #646464 via --color-gray-11 |
--elevation-5 | semantic every component | 0 via --shadow-offset-x |
--radius-md | semantic every component | 0.5rem via --radius-200 |
--radius-sm | semantic every component | 0.25rem via --radius-100 |
--spacing-100 | primitive every component | 0.25rem |
--spacing-200 | primitive every component | 0.5rem |
--spacing-300 | primitive every component | 0.75rem |
--spacing-400 | primitive every component | 1rem |
--spacing-500 | primitive every component | 1.5rem |
--spacing-700 | primitive every component | 3rem |