Widget events
The three events `<suo-search>` dispatches — suo:open, suo:select and suo:error — and how to type them in React.
<suo-search> dispatches exactly three custom events, and no others in v1: suo:open, suo:select and suo:error. Each is a native CustomEvent, dispatched on the <suo-search> element itself with bubbles: true and composed: true, so a listener on any ancestor — including one outside the element's shadow root — observes it without reaching into the shadow DOM.
suo:open
Fires when the widget becomes visible to the visitor: when a modal widget opens (via show(), toggle(), the ⌘K hotkey, or the built-in affordance), and once for an inline widget as soon as it mounts. It does not fire again while already open — calling show() on an already-open modal is a no-op and dispatches nothing.
type SuoOpenEventDetail = { mode: 'modal' | 'inline' };An inline widget dispatches this on connect, deferred to a microtask so a listener attached immediately after inserting the element — the common appendChild then addEventListener order — still catches it.
suo:select
Fires when the visitor opens a result, right before the widget navigates there. This is the same moment <suo-search> sends its own click event to POST /1/events/click (see Click events) — suo:select is for your own page, the click beacon is for reading analytics, and the two are independent of each other.
type SuoSelectEventDetail = {
objectID: string;
position: number;
href: string;
newTab: boolean;
queryID: string | null;
};position is the zero-based index of the result in the flattened, currently-visible list. queryID is null when the originating search was not eligible for click analytics — see Click events for when that happens. newTab is true when the visitor ⌘/Ctrl-clicked or middle-clicked the result, in which case the widget opens a new tab instead of navigating the current one.
Selecting a recent search or a popular search suggestion re-runs a search instead of opening a result, so it does not dispatch suo:select.
suo:error
Fires when a search request fails and the widget has no earlier results to fall back on, so it shows its built-in error panel instead. A request that fails while the widget is still showing a previous, successful response does not fire this event — that shows the quieter "showing your last results" notice instead, not the full error state.
type SuoErrorEventDetail = { message: string };message is the same user-facing string the error panel renders — plain text, already appropriate to show a visitor, not an error code to branch on.
Listening in vanilla JS
const search = document.querySelector('suo-search');
search.addEventListener('suo:open', (event) => {
console.log('opened as', event.detail.mode);
});
search.addEventListener('suo:select', (event) => {
console.log('opened result', event.detail.objectID, 'at position', event.detail.position);
});
search.addEventListener('suo:error', (event) => {
console.log('search failed:', event.detail.message);
});@suo/widget exports SuoSearchElement, which types addEventListener/removeEventListener for these three names — a listener declared as (event: CustomEvent<{ mode: WidgetMode }>) => void on 'suo:open' type-checks, and a mismatched detail shape does not.
Listening in React
@suo/react's <SuoSearch> exposes the same three events as props, so you never call addEventListener yourself:
import { SuoSearch } from '@suo/react';
function DocsSearch() {
return (
<SuoSearch
host="https://api.usesuo.com"
apiKey="suo_search_YOUR_KEY"
indexName="docs"
onOpen={({ mode }) => track('search_opened', { mode })}
onSelect={({ objectID, position }) => track('search_result_opened', { objectID, position })}
onError={({ message }) => reportError(message)}
/>
);
}onOpen, onSelect and onError are optional. <SuoSearch> always calls whichever function is currently passed as the prop — passing a new inline function on every render is fine and does not re-attach a listener to the underlying element on every render.
What this does not cover
There is no event for a keystroke, a raw query change, or a zero-results state — build against useSuoSearch from @suo/react, or @suo/client directly, if you need to react to the widget's state as the visitor types rather than just its three lifecycle moments.