Use this page when the default widget UI is not the right fit and you want to own the rendering layer yourself.Documentation Index
Fetch the complete documentation index at: https://docs.open.cx/llms.txt
Use this file to discover all available pages before exploring further.
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 withWidgetProvider, then render your own chat UI with the hooks and components you need.
App.tsx
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
| Prop | Type | Required | Description |
|---|---|---|---|
options | WidgetConfig | yes | Same shape as the default <Widget> options — see Configuration. |
children | React.ReactNode | yes | Your custom widget UI. |
components | WidgetComponentType[] | no | Custom renderers keyed by slot (see Types Reference). |
storage | ExternalStorage | no | Override the default browser storage adapter. |
loadingComponent | React.ReactNode | no | UI shown while the widget engine initializes. Distinct from the loading custom component, which renders during bot replies. |
WidgetTriggerProvider
Scopes useWidgetTrigger so a trigger button can live outside the widget surface while still toggling the widget open.
Props
| Prop | Type | Required | Description |
|---|---|---|---|
children | React.ReactNode | yes | Any subtree that needs to read or toggle isOpen. |
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.
| Field | Type | Description |
|---|---|---|
messagesState.messages | WidgetMessageU[] | Ordered message list for the active session. |
messagesState.isSendingMessage | boolean | True while the user’s message is in flight. |
messagesState.isSendingMessageToAI | boolean | True while awaiting the AI pass that follows a send. |
messagesState.lastAIResMightSolveUserIssue | boolean | Hint that the last AI reply likely resolved the question — surface CSAT off this. |
messagesState.isInitialFetchLoading | boolean | True during the first history fetch. |
sendMessage(input) | (input) => Promise<void> | Send a user message. content required; the rest optional. |
useSessions()
Access session state, switch sessions, resolve the current session, and persist state checkpoints.
| Field | Type | Description |
|---|---|---|
sessionState.session | SessionDto | null | The active session, if any. |
sessionState.isCreatingSession | boolean | True while creating a new session. |
sessionState.isResolvingSession | boolean | True while closing the active session. |
sessionsState.data | SessionDto[] | All fetched sessions, newest-first. |
sessionsState.cursor | string | undefined | Pagination cursor for loadMoreSessions. |
sessionsState.isLastPage | boolean | True when no more pages remain. |
sessionsState.didStartInitialFetch | boolean | True once the initial fetch has been kicked off. |
sessionsState.isInitialFetchLoading | boolean | True during the initial fetch. |
loadMoreSessions() | () => Promise<void> | Fetch the next page into sessionsState.data. |
resolveSession() | () => Promise<...> | Close the active session. Returns a discriminated union keyed on success. |
createStateCheckpoint(payload) | (payload) => Promise<...> | Attach a structured state payload to the current session — used by guided modes. |
openSessions | SessionDto[] | Convenience slice: sessions with isOpened: true. |
closedSessions | SessionDto[] | Convenience slice: sessions with isOpened: false. |
canCreateNewSession | boolean | Whether a new session can be started right now. |
useCsat()
Drive the CSAT survey flow — detect when the server requested feedback and submit a score.
| Field | Type | Description |
|---|---|---|
submitCsat(body) | ({ score, feedback? }) => Promise<...> | Submit a CSAT score with optional free-text feedback. |
csatRequestedMessage | WidgetSystemMessage__CsatRequested | undefined | The system message that triggered the CSAT prompt. |
isCsatRequested | boolean | True when the server has asked for CSAT but it hasn’t been submitted. |
csatSubmittedMessage | WidgetSystemMessage__CsatSubmitted | undefined | The system message recording the submitted score. |
isCsatSubmitted | boolean | True once the score has been submitted for this session. |
submittedScore | number | null | undefined | The submitted score, if any. |
submittedFeedback | string | null | undefined | The submitted feedback text, if any. |
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.
| Field | Type | Description |
|---|---|---|
widgetCtx | WidgetCtx | Internal widget context. Prefer the scoped hooks above. |
config | WidgetConfig | The live config passed to WidgetProvider. |
components | WidgetComponentType[] | undefined | Custom components registered on the provider. |
componentStore | ComponentRegistry | Registry used internally to resolve renderers by key. |
version | string | Widget engine version — useful for debugging. |
contentIframeRef | RefObject<HTMLIFrameElement | null> | Ref to the content iframe when the widget renders inside one. |
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.
| Field | Type | Description |
|---|---|---|
contactState.contact | { token; externalId? } | null | Current contact, or null if none. |
contactState.extraCollectedData | Record<string, string> | undefined | Any extra data collected alongside the contact. |
contactState.isCreatingUnverifiedContact | boolean | True while creating an unverified contact. |
contactState.isErrorCreatingUnverifiedContact | boolean | True if the last create attempt failed. |
createUnverifiedContact(payload, extra?) | (payload, extra?) => Promise<void> | Create an unverified contact with optional email, name, and custom data. |
useDocumentDir()
Resolved text direction — 'ltr' or 'rtl' — so your custom UI can mirror it.
Navigation & trigger
useWidgetRouter()
Navigate between the built-in screens (sessions list ↔ chat) from your custom UI.
| Field | Type | Description |
|---|---|---|
routerState.screen | ScreenU | The current screen identifier. |
toSessionsScreen() | () => void | Switch to the sessions list. |
toChatScreen(sessionId?) | (sessionId?) => void | Switch to a chat screen. Pass a session id to open a specific session; omit to open the active one. |
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.
| Field | Type | Description |
|---|---|---|
modes | { id; name; slug? }[] | All modes configured for this widget. |
modesComponents | ModeComponent[] | undefined | Mode components registered on the provider. |
activeModeId | string | null | undefined | Id of the mode currently driving the flow. |
activeMode | { id; name; slug? } | undefined | The resolved active mode record. |
Component | (props: ModeComponentProps) => ReactElement | undefined | Component to render for the active mode. Render null when undefined. |
useUploadFiles()
Manage attachment uploads — queue, progress, cancel, retrieve URLs — to pair with useMessages().sendMessage.
| Field | Type | Description |
|---|---|---|
allFiles | FileWithProgress[] | Every queued upload, in any status. |
appendFiles(files) | (File[]) => void | Queue new files for upload. |
handleCancelUpload(id) | (string) => void | Cancel an in-flight upload by id. |
successFiles | FileWithProgress[] | Convenience slice: only successfully uploaded files. |
emptyTheFiles() | () => void | Clear the queue (e.g. after attaching to a message). |
getFileById(id) | (string) => FileWithProgress | undefined | Look up a single entry. |
getUploadProgress(id) | (string) => number | Upload progress (0–100) for a file. |
getUploadStatus(id) | (string) => FileWithProgress['status'] | undefined | Current status for a file. |
hasErrors | boolean | True if any file in the queue errored. |
isUploading | boolean | True while any file is still uploading. |
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.
@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.
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 toWidgetProvider.
Signature
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
Own the fallback UI
Own the fallback UI
Decide what visitors should see when no custom action renderer matches, when a payload is incomplete, or when a session is resolved.
Keep action names stable
Keep action names stable
Your renderer depends on action names and payload shapes staying predictable.
Test history and identity
Test history and identity
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.
Related Documentation
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.