Docs
🪄 ⏐ Examples
Component Catalog

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 reaviz

Defining 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:

TypeWhen
invalid_jsonMalformed JSON or missing type field
unknown_componentComponent name not found in catalog
invalid_propsZod schema validation failed
render_errorRuntime 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:

  1. JSON parse — checks for syntactically valid JSON
  2. Structure check — verifies type is a string
  3. Catalog lookup — confirms the component exists in definitions
  4. Zod validation — validates and strips props through the Zod schema
  5. 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).