The main component. Wraps a LiveKit session, mounts the avatar UI, routes agent commands, manages display state. Lives at @livelayer/react.
import { AvatarWidget } from "@livelayer/react";
import "@livelayer/react/styles.css";
<AvatarWidget agentId="agt_abc123" />
| Name | Type | Description |
|---|
| agentId* | string | The published agent ID from app.livelayer.studio. |
| apiKey | string | Optional API key for cross-origin auth. Usually unnecessary; only set this if you self-host or use a custom session endpoint. |
| baseUrl | string | API base URL. Default: https://app.livelayer.studio. Override for self-hosted. |
| sessionEndpoint | string | Custom endpoint that returns { url, token } for LiveKit session. Use this when your server mints tokens. |
| sessionBody | Record<string, unknown> | Extra body fields included in the session POST. Useful for passing your own auth context (userId, plan, etc.). |
The widget can be in three states: hidden, minimized (bubble), or expanded (full panel).
| Name | Type | Description |
|---|
| displayMode | "hidden" | "minimized" | "expanded" | Controlled display state. Pass this to manage display from outside the component. |
| defaultDisplayMode | "hidden" | "minimized" | "expanded" | Initial mode before the user toggles. Default: "expanded". |
| onDisplayModeChange | (mode: DisplayMode) => void | Callback fired when the user toggles between minimized/expanded/hidden. |
| experienceMode | "WIDGET" | "EMBEDDED" | Layout type. WIDGET = floating, position-aware. EMBEDDED = inline, fills parent. |
| position | "bottom-right" | "bottom-left" | "top-right" | "top-left" | "custom" | Where the floating bubble docks. Use "custom" + className/style for full position control. |
| mobileBreakpoint | number | false | Width threshold (px) for mobile layout. Default: 640. Pass false to disable mobile layout entirely. |
| persistKey | string | localStorage key for saving display mode. Default: "ll-widget". Use distinct keys when rendering multiple widgets. |
| disablePersistence | boolean | Skip localStorage persistence entirely. |
<AvatarWidget
agentId="agt_abc123"
branding={{
logoUrl: "/logo.svg",
productName: "Acme Assistant",
primaryColor: "#0EA5E9",
accentColor: "#06B6D4",
backgroundColor: "#FFFFFF",
textColor: "#0F172A",
}}
/>
| Name | Type | Description |
|---|
| branding | BrandingConfig | Logo, product name, color palette. See BrandingConfig type. |
| avatarImageUrl | string | Override the agent's still avatar image (shown when video isn't loaded yet). |
| idleLoopUrl | string | Override the idle animation video URL. |
| agentName | string | Override the displayed agent name. |
| greeting | string | Override the dashboard-configured greeting on connect. |
Restrict what page actions the agent is allowed to perform. Same idea as the script-tag capabilities, but type-safe and finer-grained — the React package supports form filling.
import type { AgentCapability } from "@livelayer/react";
const capabilities: AgentCapability[] = [
"navigate",
"scroll",
"click",
"fill_forms",
"submit_forms",
"read_page",
];
<AvatarWidget agentId="agt_abc123" capabilities={capabilities} />
| Name | Type | Description |
|---|
| capabilities | AgentCapability[] | Allowlist of agent actions. Omit for unrestricted (back-compat). Available: navigate, scroll, click, fill_forms, submit_forms, read_page. |
| allowMic | boolean | Show the mic button in the toolbar. Default: true. |
| allowCamera | boolean | Show the camera button. Default: false (most agents are voice-only). |
| allowScreenShare | boolean | Show the screen-share button. Default: false. |
| allowTyping | boolean | Show the text input. Default: true. |
For Next.js App Router, React Router v6+, or any SPA where the URL changes without a full reload, pass pathname and wire onNavigate. Without these, the widget falls back to native anchor clicks.
| Name | Type | Description |
|---|
| pathname | string | REQUIRED for Next.js App Router and React Router v6+. The current pathname. The widget uses this for showOn/hideOn evaluation and to invalidate caches between routes. |
| showOn | RoutePattern[] | Patterns where the widget MAY render. RoutePattern = string (glob), RegExp, or function. Empty/omitted = render everywhere. |
| hideOn | RoutePattern[] | Patterns where the widget will NOT render. Wins over showOn. |
| onNavigate | (href: string) => void | Called when the agent emits a navigate command. Wire to router.push for SPA navigation; default falls back to anchor click + history.pushState. |
| onScrollToSelector | (selector: string, behavior?: "smooth" | "instant") => void | Called on scroll_to. Default scrolls the matched element into view. |
| onScrollPage | (direction: "up" | "down" | "top" | "bottom", behavior?: "smooth" | "instant") => void | Called on scroll_page. Default scrolls the document. |
| onClick | (selector: string) => void | Called on click commands. Default does document.querySelector(selector).click(). |
See Routing for full examples.
When the agent calls request_page_context, these props control what gets sent.
| Name | Type | Description |
|---|
| getPageContext | (extras?) => PageContext | Promise<PageContext> | Custom DOM walker. Default extracts headings, links, form fields, visible text, marked regions. Override to feed the agent your structured app state. |
| pageContextExtras | Record<string, unknown> | Free-form metadata always included in page context (e.g., currentUser, cartTotal). |
| getRoutes | () => RouteEntryInput[] | Promise<RouteEntryInput[]> | Override the route extraction. Default walks <a> elements; pass this to feed routes from your sitemap or API. |
See Page awareness for end-to-end examples.
<AvatarWidget
agentId="agt_abc123"
onConnect={() => analytics.track("agent_connected")}
onDisconnect={() => analytics.track("agent_disconnected")}
onTranscript={(entries) => myStore.setTranscript(entries)}
onAgentState={(state) => myStore.setAgentState(state)}
onConnectionStateChange={(state) => console.log("conn:", state)}
/>
| Name | Type | Description |
|---|
| onConnect | () => void | Session connected. |
| onDisconnect | () => void | Session disconnected. |
| onTranscript | (entries: TranscriptEntry[]) => void | Transcript updated. Fires for both partial and final entries. |
| onAgentState | (state: AgentState) => void | Agent state changed: idle | listening | thinking | speaking. |
| onConnectionStateChange | (state: ConnectionState) => void | Connection state changed: idle | connecting | connected | disconnected | error. |
| onAgentEvent | (e: AgentEventDetail) => void | Observability sink. Fires for every agent event (navigation, scroll, fill, submit, custom). Useful for analytics or debugging. |
| onAgentCommand | (cmd: AgentCommand) => void | Receives non-universal agent commands — anything you defined in the dashboard's tool config that isn't navigate/scroll/click/fill/submit. Use this for app-specific actions. |
<AvatarWidget
agentId="agt_abc123"
soundEffects={{
chime: true, // navigate sound
confirmation: true, // fill / submit sound
thinking: false, // optional thinking loop
}}
/>
| Name | Type | Description |
|---|
| soundEffects | SoundEffectsConfig | Toggle individual sound effects. All default to true; pass { thinking: false } etc. to silence. |
| Name | Type | Description |
|---|
| showMinimize | boolean | Show the minimize button in expanded layout. Default: true. |
| showClose | boolean | Show the close button. Default: true. |
| chromeless | boolean | Hide header + toolbar. Close button still appears. Default: false. |
| compactControls | boolean | Hide the top header and tuck secondary controls under an overflow menu. Default: false. |
| floatingChromeContainer | HTMLElement | null | Container for portaled floating chrome (side-tab, bottom bar). Default: document.body. Pass a specific container to keep chrome anchored to a card edge instead of the viewport. |
Render multiple agent personas and let the visitor switch between them — useful for multi-role demos (sales / support / engineering).
<AvatarWidget
agentId="agt_abc123"
teamMembers={[
{ id: "sales", name: "Sales", agentId: "agt_sales_xyz", avatarImageUrl: "/sales.png" },
{ id: "support", name: "Support", agentId: "agt_support_xyz", avatarImageUrl: "/support.png" },
]}
currentTeamMemberId="sales"
onTeamMemberChange={(member) => setCurrentMember(member.id)}
/>
| Name | Type | Description |
|---|
| teamMembers | TeamMember[] | List of agents the visitor can switch between. |
| currentTeamMemberId | string | Controlled current team member ID. |
| onTeamMemberChange | (member: TeamMember) => void | Callback fired on switch. |
Show a spinner overlay during avatar/voice transitions:
<AvatarWidget
agentId={agentId}
transforming={isUpdating}
transformingLabel="Updating voice…"
/>
| Name | Type | Description |
|---|
| transforming | boolean | Show the transformation overlay. |
| transformingLabel | string | Caption under the spinner. Default: "Transforming…". |
For cross-page persistence (one session that survives route changes) or shared rooms (multiple widgets, one session), bring your own session via controlledSession. See Controlled session.
| Name | Type | Description |
|---|
| controlledSession | ControlledSession | Consumer-owned session. When set, the widget does not create its own LiveKit room. You manage connect/disconnect lifecycle externally. |
| Name | Type | Description |
|---|
| className | string | Class on the wrapper element. |
| style | CSSProperties | Inline styles on the wrapper. |
| zIndex | number | z-index for floating chrome. Default: 2147483647 (max safe int) so the widget stacks above modals and toasts. |
The component uses forwardRef and exposes a small imperative API.
import { useRef } from "react";
import { AvatarWidget, type AvatarWidgetHandle } from "@livelayer/react";
function App() {
const ref = useRef<AvatarWidgetHandle>(null);
const sendCustomEvent = async () => {
await ref.current?.sendData({
type: "user_action",
action: "added_to_cart",
productId: "sku_123",
});
};
return <AvatarWidget ref={ref} agentId="agt_abc123" />;
}
| Name | Type | Description |
|---|
| sendData | (data: Record<string, unknown>) => Promise<void> | Publish a JSON payload over the LiveKit data channel. The agent receives it as a data message. No-op if not connected. |