Component Catalog
The Component Catalog lets LLMs render custom React components in chat responses. Instead of static markdown, the LLM emits a JSON specification inside a fenced code block and reachat validates, instantiates, and renders the matching React component automatically.
Quick Start
import { Chat, componentCatalog, SessionMessages, ChatInput } from 'reachat';
import { z } from 'zod';
const catalog = componentCatalog({
WeatherCard: {
description: 'Displays current weather for a city',
props: z.object({
city: z.string(),
temperature: z.number(),
condition: z.enum(['sunny', 'cloudy', 'rainy'])
}),
component: ({ city, temperature, condition }) => (
<div className="weather-card">
<h3>{city}</h3>
<p>
{temperature}°F — {condition}
</p>
</div>
)
}
});
function App() {
return (
<Chat sessions={sessions} activeSessionId={id} components={catalog}>
<SessionMessages />
<ChatInput />
</Chat>
);
}
const systemPrompt = catalog.systemPrompt();When the LLM responds with a fenced code block like:
```component
{ "type": "WeatherCard", "props": { "city": "San Francisco", "temperature": 68, "condition": "cloudy" } }
```reachat renders your WeatherCard React component inline in the chat.
Installation
zod is included as a regular dependency of reachat — no extra install needed.
If you use the built-in chart helper, reaviz is also required (optional peer dep).
npm install reachat
# Optional, for chart support:
npm install reavizDefining Components
Basic Component
import { z } from 'zod';
const definitions = {
AlertBox: {
description: 'Displays an alert or notification',
props: z.object({
title: z.string().describe('Alert title'),
message: z.string().describe('Alert body text'),
severity: z.enum(['info', 'warning', 'error', 'success'])
}),
component: ({ title, message, severity }) => (
<div className={`alert alert-${severity}`}>
<strong>{title}</strong>
<p>{message}</p>
</div>
)
}
};Use .describe() on Zod fields to provide hints in the generated system prompt.
Layout Component with Children
Components can receive nested children via the children prop. Register a
layout component with an empty props schema:
const definitions = {
Row: {
description:
'A horizontal flex layout — use as a parent to arrange children side-by-side',
props: z.object({}),
component: ({ children }) => (
<div style={{ display: 'flex', gap: 12 }}>{children}</div>
)
},
DataCard: {
description: 'Displays a single metric',
props: z.object({
title: z.string(),
value: z.number(),
unit: z.string().optional()
}),
component: ({ title, value, unit }) => (
<div className="data-card">
<span className="label">{title}</span>
<span className="value">
{value}
{unit}
</span>
</div>
)
}
};The LLM can then compose them:
{
"type": "Row",
"props": {},
"children": [
{
"type": "DataCard",
"props": { "title": "Revenue", "value": 12450, "unit": "$" }
},
{ "type": "DataCard", "props": { "title": "Users", "value": 8423 } }
]
}Interactive Component with sendMessage
Every component receives a sendMessage callback from the chat context. Use it
to let rendered components send follow-up messages:
const definitions = {
ActionButton: {
description: 'A button that sends a follow-up chat message when clicked',
props: z.object({
label: z.string(),
message: z.string()
}),
component: ({ label, message, sendMessage }) => (
<button onClick={() => sendMessage?.(message)}>{label}</button>
)
}
};Built-in Chart Component
reachat ships a ready-made chart component definition powered by
reaviz (opens in a new tab) that you can register in your
catalog with createChartComponentDef(). It supports bar, line, area, pie,
and sparkline chart types out of the box.
See the Charts page for setup, configuration, and examples.
JSON Spec Format
The LLM emits JSON inside a fenced code block tagged with the configured
language (default: component).
Single Component
```component
{ "type": "WeatherCard", "props": { "city": "NYC", "temperature": 45 } }
```Multiple Components (array)
```component
[
{ "type": "WeatherCard", "props": { "city": "NYC", "temperature": 45 } },
{ "type": "WeatherCard", "props": { "city": "LA", "temperature": 78 } }
]
```Nested Children
```component
{
"type": "Row",
"props": {},
"children": [
{ "type": "DataCard", "props": { "title": "Revenue", "value": 9000 } },
{ "type": "DataCard", "props": { "title": "Users", "value": 420 } }
]
}
```Wiring into Chat
Simple (recommended)
Pass the catalog to the components prop. This automatically sets up the remark
plugin and markdown component overrides:
<Chat sessions={sessions} activeSessionId={id} components={catalog}>
<SessionMessages />
<ChatInput />
</Chat>Advanced (manual wiring)
For fine-grained control, use the catalog's individual pieces:
<Chat
sessions={sessions}
activeSessionId={id}
remarkPlugins={[remarkGfm, catalog.remarkPlugin]}
markdownComponents={{ ...catalog.components, ...myOtherOverrides }}
>
<SessionMessages />
<ChatInput />
</Chat>System Prompt Generation
Call catalog.systemPrompt() to get a pre-formatted instruction string that
tells the LLM which components are available, what props they accept, and how to
format the JSON. Include this in your LLM system message:
const systemPrompt = catalog.systemPrompt();Custom Language Tag
Use a different code block language if component conflicts with your setup:
const catalog = componentCatalog(definitions, { language: 'ui' });The LLM will then use ```ui blocks instead.
Error Handling
The system validates every JSON spec before rendering. Four error types exist:
| Type | When |
|---|---|
invalid_json | Malformed JSON or missing type field |
unknown_component | Component name not found in catalog |
invalid_props | Zod schema validation failed |
render_error | Runtime error during React rendering |
Default Error UI
By default, errors render as an inline error box via ComponentError.
Custom Error Handler
Provide an onError callback to replace or suppress the default UI:
const catalog = componentCatalog(definitions, {
onError: error => {
if (error.type === 'unknown_component') {
return <div>Component not available: {error.componentType}</div>;
}
return undefined;
}
});Error Boundary
Each rendered component is wrapped in a React error boundary. If a component throws during rendering, the error is caught and displayed without crashing the rest of the chat interface.
Validation Pipeline
The validation pipeline runs in order:
- JSON parse — checks for syntactically valid JSON
- Structure check — verifies
typeis a string - Catalog lookup — confirms the component exists in definitions
- Zod validation — validates and strips props through the Zod schema
- Recursive children — repeats steps 2–4 for nested children
All validation happens via validateSpec(), which is also exported for direct
use in testing or server-side validation:
import { validateSpec } from 'reachat';
const result = validateSpec(jsonString, catalog.definitions);
if (result.ok) {
console.log('Valid specs:', result.specs);
} else {
console.error('Validation error:', result.error);
}Full Example
import { z } from 'zod';
import {
Chat,
SessionMessages,
ChatInput,
SessionMessagePanel,
componentCatalog,
createChartComponentDef
} from 'reachat';
const catalog = componentCatalog({
WeatherCard: {
description: 'Displays current weather conditions for a city',
props: z.object({
city: z.string().describe('City name'),
temperature: z.number().describe('Temperature in Fahrenheit'),
condition: z
.enum(['sunny', 'cloudy', 'rainy'])
.describe('Weather condition')
}),
component: ({ city, temperature, condition }) => (
<div className="p-4 rounded-xl bg-gradient-to-br from-blue-400 to-blue-600 text-white">
<div className="text-sm opacity-80">{condition}</div>
<div className="text-2xl font-bold">{city}</div>
<div className="text-4xl font-light">{temperature}°F</div>
</div>
)
},
Chart: createChartComponentDef(),
Row: {
description: 'Horizontal flex layout for arranging children side-by-side',
props: z.object({}),
component: ({ children }) => (
<div className="flex gap-3 flex-wrap">{children}</div>
)
}
});
function App() {
return (
<Chat
sessions={sessions}
activeSessionId={activeId}
components={catalog}
onSendMessage={handleSend}
>
<SessionMessagePanel>
<SessionMessages />
<ChatInput />
</SessionMessagePanel>
</Chat>
);
}
async function handleSend(message: string) {
const response = await callLLM({
system: catalog.systemPrompt(),
messages: [{ role: 'user', content: message }]
});
}For more examples and advanced usage, visit the storybook demos (opens in a new tab).