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.
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.
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.
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
@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:
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.