Skip to main content
Use this page when the default widget UI is not the right fit and you want to own the rendering layer yourself.
Need the broader concept first? Start with Custom Components.
The headless path skips the default UI, but the widget options behave the same. Use the Playground to validate option shapes live before wiring them into your headless setup.

How Headless Component Rendering Works

For a headless implementation, you wire the widget engine into your own UI and decide which custom component to render for each action result. That usually means:
  • wrapping your app in
  • rendering your own message UI
  • switching on props.data.action?.name
  • owning fallback states and storage behavior

Step 1: Build The Message Component

Use so your renderer has access to the action name and payload.
AIAgentMessage.tsx

Step 2: Wire It Into The Headless Widget

Start with WidgetProvider, then render your own chat UI with the hooks and components you need.
App.tsx
From there, your custom widget can reach for the hooks below — each backed by WidgetProvider. The Hooks Reference documents every input, return field, and provider requirement.

Providers Reference

The headless package ships two providers. WidgetProvider is required; WidgetTriggerProvider is only needed when a launcher button lives outside the widget surface (e.g. in your site header).

WidgetProvider

Initializes the widget engine and powers every hook below it in the tree. Props

WidgetTriggerProvider

Scopes useWidgetTrigger so a trigger button can live outside the widget surface while still toggling the widget open. Props

Hooks Reference

Every hook must be called inside <WidgetProvider>, with one exception: useWidgetTrigger requires <WidgetTriggerProvider>.

Conversation

useMessages()

Read the message stream and send new user messages.

useSessions()

Access session state, switch sessions, resolve the current session, and persist state checkpoints.

useCsat()

Drive the CSAT survey flow — detect when the server requested feedback and submit a score.

useIsAwaitingBotReply()

Lightweight flag for “is the bot replying?” — use this for typing indicators without subscribing to the full message list.

Widget state

useWidget()

Access the low-level widget context. Prefer the higher-level hooks unless you need raw context, the component registry, or the engine version.

useConfig()

Shortcut to the live WidgetConfig. Equivalent to useWidget().config.

useContact()

Read the current contact (verified or unverified) and create an unverified contact on the fly when you collect identity inside your own UI.

useDocumentDir()

Resolved text direction — 'ltr' or 'rtl' — so your custom UI can mirror it.

useWidgetRouter()

Navigate between the built-in screens (sessions list ↔ chat) from your custom UI.

useWidgetTrigger()

Read and toggle the widget’s open/closed state from anywhere inside <WidgetTriggerProvider>. Requires: Inside <WidgetTriggerProvider>.

Modes & files

useModes()

Read the configured modes, the currently active mode, and the component that renders it.

useUploadFiles()

Manage attachment uploads — queue, progress, cancel, retrieve URLs — to pair with useMessages().sendMessage.

Utilities

usePrimitiveState<T>()

Subscribe a React component to a PrimitiveState<T> published by the widget core. Returns the current value and re-renders on change.
Use this only when wiring advanced integrations that read a primitive state directly from @opencx/widget-core. Most app code should prefer the higher-level hooks above.

Types Reference

WidgetComponentProps<TData = unknown>

Prop type passed to custom renderers. It is a discriminated union of the three message kinds — switch on the message shape (or data.action?.name for AI action results) to decide what to render.
See React Components for the action-name routing pattern.

WidgetComponentType

Registration entry for the components prop of WidgetProvider.

FileWithProgress

Queue entry returned by useUploadFiles.

Step 3: Add Storage When The Browser Default Is Not Enough

If browser storage is not the right fit, pass an to WidgetProvider. Signature
Example
This is especially useful in mobile apps, embedded environments, or custom shells where your app controls persistence itself.

Step 4: Use Modes When You Need A Guided Flow

Use when the widget should temporarily switch from normal chat to a guided flow such as onboarding, qualification, or structured data collection. Use an action-result component when you are displaying a result. Use a mode component when you are driving a flow.

What To Watch For

Decide what visitors should see when no custom action renderer matches, when a payload is incomplete, or when a session is resolved.
Your renderer depends on action names and payload shapes staying predictable.
Make sure your custom UI still handles returning visitors, resolved sessions, and verified identity correctly.

Good Headless Use Cases

Fully custom layouts

Build your own message thread, session list, composer, or embedded support layout.

Platform-specific storage

Use your own storage layer when browser defaults are not the right fit.

Action-aware rendering

Swap in different UI for different action results without relying on the default widget shell.

Guided flows

Combine headless rendering with modes when the widget needs to collect structured input over multiple steps.

Custom Components

Decide whether you need action-result UI or a full headless build.

React Components

Register custom UI on the default React widget.

Configuration

Pair headless builds with widget behavior, modes, and prompts.

Install Widget

Start with the right implementation path before building custom UI.