Ensemble Docs

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:

  1. widgetType - A unique name for the widget
  2. schema - A Zod schema (v3 or v4) or JSON Schema defining the payload structure
  3. reactDOM - Your ReactDOM instance for cross-version compatibility
  4. render() - React code to render the widget
  5. 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-dev

The package includes global type declarations for window.ChatWidget.

On this page