Custom Widgets
Render rich UI widgets in chat responses
The chat widget lets you define rich, interactive components to supplement the default markdown response.
Each widget requires:
- widgetType - A unique name for the widget
- schema - A Zod schema (v3 or v4) or JSON Schema defining the payload structure
- reactDOM - Your ReactDOM instance for cross-version compatibility
- render() - React code to render the widget
- enrich (optional) - Fetch additional data from Ensemble tools
Defining Custom Widgets
import ReactDOM from 'react-dom/client'; // or 'react-dom' for React 16/17
import { ChatWidget, createWidget } from '@ensembleapp/client-sdk';
import { z } from 'zod';
const widgets = [
createWidget({
widgetType: 'product-card',
// Zod schema with describe() helps the agent generate correct data
schema: z.object({
name: z.string(),
price: z.number(),
imageUrl: z.string().optional(),
}).describe('Display a product card'),
// Required: your ReactDOM instance for rendering
reactDOM: ReactDOM,
render: (payload) => (
<div className="border rounded p-4">
{payload.imageUrl && <img src={payload.imageUrl} alt={payload.name} />}
<h3>{payload.name}</h3>
<p>${payload.price}</p>
</div>
),
}),
];
<ChatWidget
api={{ baseUrl, token }}
agentId="shopping-agent"
threadId={threadId}
widgets={widgets}
/>Schema Formats
The schema field accepts multiple formats:
Zod (v3 or v4)
import { z } from 'zod';
schema: z.object({
name: z.string(),
price: z.number(),
imageUrl: z.string().optional(),
}).describe('Display a product card')Using .describe() on the schema helps the agent understand when to use this widget.
JSON Schema
schema: {
type: 'object',
description: 'Display a product card',
properties: {
name: { type: 'string' },
price: { type: 'number' },
imageUrl: { type: 'string' },
},
required: ['name', 'price'],
}JSON Schema is useful when you don't want to add Zod as a dependency.
Why reactDOM is Required
The SDK bundles its own React 18 for the chat UI. Your custom widgets need to render using your React version to ensure:
- Event handlers work correctly (onClick, onChange, etc.)
- React hooks and state work inside your widgets
- Context from your app is accessible
- Compatibility with React 16, 17, or 18
The SDK creates a DOM container and uses your ReactDOM to render your widget into it, allowing both React versions to coexist.
Widget Enrichment
Widgets can fetch additional data from tools to supplement the agent's response. This is useful for real-time data, large datasets, or computed values that the LLM shouldn't generate.
See Widget Enrichment for full details on server and client enrichment patterns.
Using with Script Tag (IIFE)
If you're loading the chat widget via script tag, use window.ChatWidget.createWidget():
<script src="https://cdn.example.com/chat-widget.js"></script>
<script>
// Your widgets (built separately with your React version)
const productWidget = window.ChatWidget.createWidget({
widgetType: 'product-card',
schema: {
type: 'object',
properties: {
name: { type: 'string' },
price: { type: 'number' },
},
required: ['name', 'price'],
},
reactDOM: ReactDOM, // Your ReactDOM instance
render: (payload) => React.createElement('div', null,
React.createElement('h3', null, payload.name),
React.createElement('p', null, '$' + payload.price)
),
});
window.ChatWidget.init({
api: { baseUrl: '/api', token: 'your-token' },
agentId: 'shopping-agent',
threadId: 'thread-123',
widgets: [productWidget],
});
</script>TypeScript Support for Script Tag Users
Install the SDK as a dev dependency to get type definitions:
npm install @ensembleapp/client-sdk --save-devThe package includes global type declarations for window.ChatWidget.