Embedding the chat widget (React)
Add the Runtype chat widget to a React or Next.js application as a ChatWidget component that you generate from your chat Surface.
Before you begin
Complete the following prerequisites:
- Create a Product with a chat Surface. For more information, see Setting up a chat surface.
- Create a client token for the Surface, and add your site’s origin to its Allowed origins. For more information, see Client tokens and domain restrictions.
Use a client token instead of an API key in a chat widget. Client tokens use the ct_live_ or ct_test_ prefix and are designed for client-side code. Restrict each token to your site’s origins to help protect it from reuse on other sites.
Install the package
Install the Persona package in your application with the following command:
Get the component code
Copy the generated component from your chat Surface. The steps depend on which dashboard view you use:
Workbench
Classic dashboard
- Open the chat Surface and select Embed.
- Select React.
- Select a client token in the token list, or select Create token to create one.
- If you selected a live token, paste the token that you saved when you created it into Saved live token. The dashboard stores only a hint of a live token, so it can’t show the full value again.
- Click Copy.
To have a coding agent add the widget for you, open Embed in the workbench, select your tool under Build it with a coding agent, and click Copy prompt. Paste the prompt into Claude Code, Cursor, or Codex.
Generate the component from the CLI
The runtype persona init command creates a new Agent and a client token for it, sets the token’s allowed origins, and prints a React component that connects to that Agent. Use it to start from a new Agent instead of an existing chat Surface:
Coding agents connected to the Runtype MCP server can generate the React component with the generate_persona_embed_code tool.
Add the component to your app
- Save the generated code as
ChatWidget.tsxin your application. - If your application doesn’t use the Next.js App Router, delete the
'use client'line at the top of the file. Vite and Create React App don’t need it. - Render
<ChatWidget />once, near the root of your application.
The generated component imports @runtypelabs/persona/widget.css and initializes the widget with your client token and theme. It mounts the widget to the document body, renders nothing itself, and calls handle.destroy() when React unmounts it. Customize the component for your application after you add it.
Verify the widget
Open your application in a browser. The chat launcher appears on the page. Open it and send a message to confirm that the Surface replies.
In the workbench, Origins seen in Embed lists your site’s origin as Allowed after the widget connects.
Control the widget from your code
initAgentWidget returns a handle with the following methods:
open()andclose(): show or hide the chat panel.submitMessage(text): send a message as the visitor.destroy(): remove the widget.on(event, callback): listen for widget events, such asuser:message,assistant:message,assistant:complete,widget:opened, andwidget:closed.
The generated component keeps the handle inside useEffect. To call it from elsewhere in your component, store it in a ref. The following example opens the widget from a button:
Keep the config object from your generated code rather than the shortened one in this example.
Troubleshooting
The widget appears but messages fail. A common cause is that your site’s origin isn’t in the token’s allowed origins. In the workbench, find the origin in Origins seen and click Allow, or add it in Access & keys.
The code contains YOUR_CLIENT_TOKEN. In the classic dashboard, you copied the code with a placeholder selected. Replace YOUR_CLIENT_TOKEN with your client token, or copy the code again with a token selected.
The Copy button is disabled in the workbench. No client token is selected, or you selected a live token and didn’t paste its full value. Select a token, or paste the full live token that you saved when you created it into Saved live token. If you no longer have the live token, create a new token.
Build a custom voice interface
For a voice interface built from your own React components, use useVoiceClient from @runtypelabs/voice/react. It manages microphone capture, playback, transcripts, and the Agent’s Interruption Mode setting, which controls whether a caller can interrupt a spoken reply.
Install the voice package with the following command:
The following component starts and ends a call with an Agent:
Use a client token that can access the Agent. Start calls from a user click on an HTTPS page or on localhost. The hook releases the microphone when the component unmounts. For plain JavaScript usage and the Persona widget adapter, see the @runtypelabs/voice package.
Connect ahead on mic hover
A call’s first connection and first reply are slower when the browser still has to open the voice connection. To remove part of that delay, spread getMicButtonPrewarmProps() onto the button that starts the call:
When the pointer enters the button, the button receives focus, or the user presses it, the client opens the voice connection and authenticates before the click. startCall() then reuses the open connection.
- An early connection doesn’t request microphone access, create a conversation, bill, reserve quota, or record usage. The conversation is created when the call starts.
- The client keeps one early connection and closes it after 30 seconds if no call starts.
voice.attachedistruewhile one is ready. - If the early connection drops, the call still starts normally over a new connection. Connecting early never throws.
- In a browser without
WebSocket, the client sends one request toPOST /v1/client/agents/{agentId}/voice/prewarminstead, at most once every 25 seconds. Runtype limits these requests for each client token and Agent.
For a page where voice is the main interaction, pass keepWarm: true:
With keepWarm, the hook connects when it mounts, and Runtype holds the connection for up to 10 minutes. After a drop, the client reconnects on the next mic hover or when the page becomes visible again. It never reconnects while the page is hidden.
Without React, call voice.attach() or voice.prewarm() on a VoiceClient, and call voice.dispose() when the view is removed. For a Persona widget, call bindVoicePrewarmToElement(provider, micButton) from @runtypelabs/voice/persona. This function returns a function that removes the listeners.
Next steps
- Widget theming and customization: customize the widget appearance
- Conversation history in an embedded widget: keep a visitor’s conversation history across sessions and devices with an identity proof
- Client tokens and domain restrictions: configure client token origins
- Quickstart: From Agent to Chat Widget: build an Agent and embed it as a chat widget from start to finish
- Embedding the chat widget (script tag): embed the widget on non-React sites