suo
Widget reference

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.

On this page