The plug-and-play, framework-agnostic React renderer that turns raw LLM text and JSON output into rich, interactive UI components — in real time.
When building AI chatbots, agents, or assistants, LLMs (OpenAI, Anthropic Claude, Google Gemini, Ollama) return unstructured text streams. Standard chat interfaces render this text as plain markdown paragraphs.
react-generative-ui bridges the gap between text streams and rich React components.
┌─────────────────────────────────────────────────────────────────────────────────────────┐
│ AI Output Stream (Raw Text + Embedded JSON) │
│ │
│ "Here is your monthly revenue report: │
│ {"componentName":"StatCard","props":{"title":"Revenue","value":"$48.2k","trend":"up"}} │
│ Below is the regional breakdown: │
│ {"componentName":"DataTable","props":{"headers":["Region","Sales"],"rows":[...]}}" │
└─────────────────────────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────┐
│ react-generative-ui Engine │
│ • parseBlocks() │
│ • <GenerativeRenderer /> │
└──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────────────────┐
│ Rendered React UI │
│ │
│ Here is your monthly revenue report: │
│ ┌──────────────────────────┐ │
│ │ Revenue $48.2k │ ← Interactive StatCard Component │
│ └──────────────────────────┘ │
│ Below is the regional breakdown: │
│ ┌──────────────────────────┐ │
│ │ Region │ Sales │ ← Formatted DataTable Component │
│ ├─────────┼────────────────┤ │
│ │ North │ $21,000 │ │
│ └─────────┴────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────────────────┘
| Challenge with Raw LLM JSON | How react-generative-ui Solves It |
|---|---|
| Manual JSON Extraction | parseBlocks() automatically scans text, extracts JSON blocks with brace-depth protection, and keeps surrounding markdown intact. |
| Streaming Flakiness | createStreamingParser() handles partial JSON tokens during token-by-token streaming without breaking your layout. |
| Rigid Chat Framework Lock-in | Works anywhere in standard React (Next.js, Vite, Remix). No locked-in chat runtimes or proprietary cloud dependencies. |
| Runtime Crash Risk from Bad LLM Props | Optional Zod integration validates props at runtime (withSchema) and safely falls back without crashing the app. |
| Style & Design Overhead | Includes 17 pre-designed components + a CLI (npx react-generative-ui add) to copy source code directly into your project (like shadcn/ui). |
- Quick Start
- Why react-generative-ui?
- Integration Paths
- Architecture & System Flow
- Core Concepts
- System Prompting (LLM Setup)
- Built-in Component Catalog (17 Components)
- Streaming Support
- Zod Schema Validation
- CLI Reference
- Bundle Size & Subpath Tree-Shaking
- Security Model
- Comparison Matrix
- Full API Reference
- TypeScript Reference
- Frequently Asked Questions
- Documentation Directory
- License & Version History
Build a working generative UI prototype in under two minutes:
npm install react-generative-uiCreate src/generativeUIRegistry.ts:
import {
ComponentRegistry,
withSchema,
StatCard, StatCardSchema,
AlertBox, AlertBoxSchema,
DataTable, DataTableSchema,
buildSystemPrompt,
} from 'react-generative-ui';
// 1. Map component names to React components (with optional Zod schemas)
export const registry: ComponentRegistry = {
StatCard: withSchema(StatCard, StatCardSchema),
AlertBox: withSchema(AlertBox, AlertBoxSchema),
DataTable: withSchema(DataTable, DataTableSchema),
};
// 2. Generate a system prompt that teaches the LLM how to format JSON blocks
export const systemPrompt = buildSystemPrompt(registry, {
StatCard: { title: 'Revenue', value: '$48,200', change: '+12%', trend: 'up' },
AlertBox: { type: 'warning', title: 'Notice', message: 'Action required' },
DataTable: { title: 'Sales', headers: ['Region', 'Total'], rows: [{ Region: 'North', Total: '$10k' }] },
});import { useState } from 'react';
import { parseBlocks, GenerativeRenderer, UIBlock } from 'react-generative-ui';
import { registry, systemPrompt } from './generativeUIRegistry';
export default function GenerativeChatApp() {
const [blocks, setBlocks] = useState<UIBlock[]>([]);
async function handleSendUserMessage(userQuery: string) {
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.NEXT_PUBLIC_OPENAI_KEY}`,
},
body: JSON.stringify({
model: 'gpt-4o',
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: userQuery },
],
}),
});
const data = await response.json();
const rawAiText = data.choices[0].message.content;
// Parse mixed text + JSON blocks
const parsedBlocks = parseBlocks(rawAiText);
setBlocks(parsedBlocks);
}
return (
<div className="chat-container">
<button onClick={() => handleSendUserMessage('Show revenue stats')}>
Fetch Data
</button>
{/* Render plain text paragraphs and dynamic UI components seamlessly */}
<GenerativeRenderer blocks={blocks} registry={registry} />
</div>
);
}LLMs excel at structured JSON and natural language prose. However, bridging them in frontend code usually requires fragile custom regex, manual component routing, or adopting heavy opinionated chat frameworks that force specific backend architectures.
react-generative-ui is built on three core principles:
- Framework Independence: Zero opinion on state management, backend APIs, or chat runtimes. Works with standard React
useState, TanStack Query, Vercel AI SDK, or WebSocket streams. - Code Ownership (Shadcn-Style CLI): Use components directly from NPM or copy the raw React + CSS source code into your project via
npx react-generative-ui addto customize styling completely. - Fail-Safe Robustness: Custom brace-depth scanner, input length limits, sanitization against XSS (
javascript:links blocked), and silent Zod error recovery ensure bad AI output never breaks your UI.
Choose the workflow that fits your team's design and customization requirements:
┌─────────────────────────────────────────┐
│ How do you want to manage UI components?│
└────────────────────┬────────────────────┘
│
┌─────────────────────────────┼─────────────────────────────┐
▼ ▼ ▼
Path A: Install Path B: Copy Path C: Manual
(Pure NPM Import) (Scaffold via CLI) (Custom Components)
───────────────── ────────────────── ───────────────────
• Import from npm • Scaffolds code into repo • Use your existing design
• Fast startup • Full Tailwind/CSS control system or UI library
• Zero setup • Modify logic & props • Pure component mapping
| Dimension | Path A: Install | Path B: Copy (Recommended) | Path C: Manual |
|---|---|---|---|
| Setup Time | ~1 minute | ~3 minutes | ~5 minutes |
| Code Ownership | NPM Node Modules | Your Repository (/src/components) |
Your Repository |
| Custom Styling | CSS overrides | Full TSX + CSS control | Full TSX + CSS control |
| CLI Usage | None | npx react-generative-ui add |
None |
| Best Used For | Prototyping & standard dark/light themes | Production apps needing custom brand designs | Integrating internal design systems (MUI, Shadcn, Chakra) |
Import components directly from the package root:
import { GenerativeRenderer, parseBlocks, withSchema } from 'react-generative-ui';
import { StatCard, StatCardSchema } from 'react-generative-ui';
const registry = {
StatCard: withSchema(StatCard, StatCardSchema),
};Scaffold a registry file and copy component source files into your project:
# 1. Initialize starter registry file (src/generative-ui-registry.ts)
npx react-generative-ui init
# 2. Copy component source code directly into src/components/generative-ui/
npx react-generative-ui add stat-card data-table alert-box pro-con-tableEdit the generated .tsx files inside src/components/generative-ui/ to match your exact brand guidelines and styling rules.
Map any custom React component directly in the registry:
const CustomMetricCard: React.FC<{ label: string; amount: number }> = ({ label, amount }) => (
<div className="p-4 bg-slate-900 rounded-lg text-white">
<span className="text-sm text-slate-400">{label}</span>
<h3 className="text-2xl font-bold">${amount.toLocaleString()}</h3>
</div>
);
const registry = {
CustomMetricCard,
};Understanding how react-generative-ui transforms raw string streams into rendered component trees:
flowchart TD
A["LLM Text Output Stream"] --> B["parseBlocks / createStreamingParser"]
subgraph Parsing Engine
B --> C{"Brace Scanner & Depth Guard"}
C -->|"Plain Text"| D["Text Block: props.content"]
C -->|"Valid JSON with componentName"| E["UIBlock Object"]
C -->|"Malformed JSON in strict mode"| F["Dropped or Raw Fallback"]
end
D --> G["UIBlock Array"]
E --> G
G --> H["GenerativeRenderer Component"]
subgraph Rendering Pipeline
H --> I{"Lookup componentName in Registry"}
I -->|"Found: Plain FC"| J["Render React Component"]
I -->|"Found: withSchema Entry"| K{"Zod safeParse Validation"}
K -->|"Success"| J
K -->|"Validation Error"| L{"Fallback Component Provided?"}
I -->|"Not Found"| L
L -->|"Yes"| M["Render Custom Fallback Component"]
L -->|"No"| N["Silent Skip / Console Warning"]
end
J --> O["Final Interactive DOM"]
M --> O
The fundamental data structure. Every segment of an AI response is normalized into a UIBlock:
interface UIBlock {
/** Matches a key string in your ComponentRegistry */
componentName: string;
/** Props passed directly into the React component */
props: Record<string, unknown>;
/** Unique key for React list rendering (auto-generated if omitted) */
id?: string;
}- Plain text between JSON segments is converted to
{ componentName: "Text", props: { content: "..." } }. - JSON objects with
"componentName"are converted to{ componentName: "StatCard", props: { ... } }.
A key-value dictionary mapping string identifiers to React components or schema-wrapped entries:
type ComponentRegistry = Record<
string,
React.FC<any> | RegistryEntry<any>
>;The parsing function scans raw input text for JSON blocks. It includes:
- Brace-Depth Protection: Prevents stack overflow attacks from deeply nested braces (
maxDepthdefault: 50). - Length Limit Guard: Protects memory by rejecting inputs larger than
maxInputLength(default: 1 MB). - Strict & Lenient Modes:
strict: true(default) drops malformed JSON;strict: falserenders bad JSON as raw text.
To ensure your LLM generates correctly formatted JSON blocks alongside markdown prose, use buildSystemPrompt() or getSystemPromptInstruction().
import { buildSystemPrompt } from 'react-generative-ui';
const systemPrompt = buildSystemPrompt(
registry,
{
StatCard: { title: 'Total Revenue', value: '$48,200', change: '+12%', trend: 'up' },
DataTable: { title: 'Top Products', headers: ['Name', 'Price'], rows: [{ Name: 'Widget', Price: '$99' }] },
},
'You are an executive AI assistant. Present clear text explanations accompanied by visual UI cards.'
);You are an executive AI assistant. Present clear text explanations accompanied by visual UI cards.
When you need to display structured data, statistics, comparisons, or visual information,
output a JSON block using the following format EXACTLY (do not wrap in markdown code fences):
{"componentName": "<ComponentName>", "props": { <component specific props> }}
Available components and their formats:
- StatCard: {"componentName":"StatCard","props":{"title":"Total Revenue","value":"$48,200","change":"+12%","trend":"up"}}
- DataTable: {"componentName":"DataTable","props":{"title":"Top Products","headers":["Name","Price"],"rows":[{"Name":"Widget","Price":"$99"}]}}
Rules:
1. Only use the component names listed above. Do NOT invent new names.
2. You can mix normal text and JSON blocks in a single response.
3. JSON blocks must be valid JSON — double-quote all keys and string values.
4. If you are unsure which component to use, just respond with normal text.
react-generative-ui includes 17 production-ready default templates.
| Component | Description | Extra Dependencies | Subpath Import |
|---|---|---|---|
| StatCard | KPI metric card with trend indicator & icon | None | react-generative-ui/components/stat-card |
| DataTable | Structured data table with borders & headers | None | react-generative-ui/components/data-table |
| KeyValueList | Metadata label-value pairs | None | react-generative-ui/components/key-value-list |
| ProConTable | Two-column pros and cons side-by-side table | None | react-generative-ui/components/pro-con-table |
| ComparisonTable | Feature grid comparing multiple products | None | react-generative-ui/components/comparison-table |
| BarChart | Vertical bar chart for quantitative series | recharts |
react-generative-ui/components/bar-chart |
| LineChart | Line/area trend chart over time | recharts |
react-generative-ui/components/line-chart |
| PieChart | Donut/Pie distribution chart | recharts |
react-generative-ui/components/pie-chart |
| AlertBox | Status notice banner (info, success, warning, error) | None | react-generative-ui/components/alert-box |
| Badge | Inline status tag or category pill | None | react-generative-ui/components/badge |
| ProgressBar | Linear completion indicator bar | None | react-generative-ui/components/progress-bar |
| Timeline | Chronological milestone event list | None | react-generative-ui/components/timeline |
| Accordion | Collapsible FAQ items panel | None | react-generative-ui/components/accordion |
| CodeBlock | Tokenized syntax highlighted code viewer | None | react-generative-ui/components/code-block |
| SourceList | Citation list with URL security validation | None | react-generative-ui/components/source-list |
| QuickReplyButtons | Interactive action prompt pills | None (Needs callback) | react-generative-ui/components/quick-reply-buttons |
| ConfirmationCard | Action approval/deny decision card | None (Needs callback) | react-generative-ui/components/confirmation-card |
KPI metric display with titles, values, change percentages, trend arrows, and icons.
{
"componentName": "StatCard",
"props": {
"title": "Monthly Active Users",
"value": "128,450",
"change": "+14.2%",
"trend": "up",
"icon": "users"
}
}Tabular grid for structured records.
{
"componentName": "DataTable",
"props": {
"title": "Top Regional Sales",
"headers": ["Region", "Rep", "Revenue"],
"rows": [
{ "Region": "North America", "Rep": "Sarah Jenkins", "Revenue": "$142,000" },
{ "Region": "Europe", "Rep": "Marco Rossi", "Revenue": "$98,500" }
]
}
}Bar visualization for numeric category comparisons.
{
"componentName": "BarChart",
"props": {
"title": "Quarterly Revenue Growth",
"data": [
{ "label": "Q1", "value": 45000 },
{ "label": "Q2", "value": 58000 },
{ "label": "Q3", "value": 72000 }
],
"color": "#6366f1"
}
}Because functions cannot be passed in raw LLM JSON, interactive components (QuickReplyButtons, ConfirmationCard) use a wrapped component pattern:
import { QuickReplyButtons } from 'react-generative-ui';
const ConnectedQuickReplies: React.FC<any> = (props) => (
<QuickReplyButtons
{...props}
onSelect={(optionId: string) => {
console.log('User clicked option:', optionId);
// Trigger new chat message or workflow
}}
/>
);
export const registry = {
QuickReplyButtons: ConnectedQuickReplies,
};When handling token-by-token HTTP stream responses (Server-Sent Events), use createStreamingParser() to buffer partial tokens and render completed blocks in real time.
sequenceDiagram
participant LLM as LLM Stream (SSE)
participant App as React App Loop
participant Parser as createStreamingParser
participant UI as <GenerativeRenderer />
LLM->>App: chunk: "Here is your metric: {"
App->>Parser: push("Here is your metric: {")
Parser-->>App: returns [TextBlock("Here is your metric:")]
App->>UI: setBlocks([...prev, TextBlock])
LLM->>App: chunk: '"componentName":"StatCard","props":{"value":"42"}}'
App->>Parser: push('"componentName":"StatCard","props":{"value":"42"}}')
Parser-->>App: returns [StatCardBlock]
App->>UI: setBlocks([...prev, StatCardBlock])
LLM->>App: Stream Closed
App->>Parser: flush()
Parser-->>App: returns remaining trailing text (if any)
import { useState } from 'react';
import { createStreamingParser, GenerativeRenderer, UIBlock } from 'react-generative-ui';
import { registry, systemPrompt } from './generativeUIRegistry';
export default function StreamingChat() {
const [blocks, setBlocks] = useState<UIBlock[]>([]);
async function startStreamingChat(prompt: string) {
const parser = createStreamingParser({ strict: true });
setBlocks([]);
const response = await fetch('/api/chat-stream', {
method: 'POST',
body: JSON.stringify({ prompt, systemPrompt }),
});
const reader = response.body!.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value, { stream: true });
const newParsedBlocks = parser.push(chunk);
if (newParsedBlocks.length > 0) {
setBlocks((prev) => [...prev, ...newParsedBlocks]);
}
}
// Flush any remaining buffer when stream finishes
const trailingBlocks = parser.flush();
if (trailingBlocks.length > 0) {
setBlocks((prev) => [...prev, ...trailingBlocks]);
}
}
return (
<div>
<button onClick={() => startStreamingChat('Generate summary report')}>
Start Stream
</button>
<GenerativeRenderer blocks={blocks} registry={registry} />
</div>
);
}Validate LLM props at runtime before rendering to ensure bad types (e.g. string passed instead of number array) don't throw React rendering errors.
import { z } from 'zod';
import { withSchema, GenerativeRenderer } from 'react-generative-ui';
// 1. Define component prop schema
export const CustomChartSchema = z.object({
title: z.string(),
values: z.array(z.number()),
theme: z.enum(['dark', 'light']).default('dark'),
});
type CustomChartProps = z.infer<typeof CustomChartSchema>;
const CustomChart: React.FC<CustomChartProps> = ({ title, values, theme }) => (
<div className={`chart-${theme}`}>
<h4>{title}</h4>
<p>Points: {values.join(', ')}</p>
</div>
);
// 2. Register with withSchema() helper
export const registry = {
CustomChart: withSchema(CustomChart, CustomChartSchema),
};LLM sends bad props ──► schema.safeParse(props) ──► success: false
│
┌─────────────────────────────────────────┴────────────────────────┐
▼ ▼
debug: true (Development) Custom Fallback Provided?
Logs warning with Zod issue details ├─► Yes: Renders <Fallback block={block} />
└─► No: Silently skips block
The CLI powers Path B (Copy) workflow, copying component source code directly into your repository.
npx react-generative-ui <command> [options]Scaffolds a starter src/generative-ui-registry.ts file configured with 4 default components and ready-to-use system prompt generation.
npx react-generative-ui initCopies the .tsx component file and .schema.ts file into your local project directory.
# Add a single component
npx react-generative-ui add stat-card
# Add multiple components
npx react-generative-ui add stat-card alert-box data-table pro-con-table
# Add all 17 default components
npx react-generative-ui add --allLists all 17 available default components with descriptions and dependency notes.
npx react-generative-ui list| Flag | Description | Default |
|---|---|---|
--dir <path> |
Directory path where components will be written | ./src/components/generative-ui/ |
--overwrite / -y / --yes |
Skip overwrite prompt confirmations (ideal for CI / agents) | false |
--all |
Scaffold all available components at once | false |
react-generative-ui supports granular subpath imports to keep your production bundle lean.
import { StatCard, AlertBox } from 'react-generative-ui';Includes main package engine + schema definitions. Components tree-shake, but schemas are included.
// Only StatCard source + schema is included in bundle
import { StatCard, StatCardSchema } from 'react-generative-ui/components/stat-card';
import { AlertBox, AlertBoxSchema } from 'react-generative-ui/components/alert-box';
import { withSchema } from 'react-generative-ui';Handling un-sanitized content generated by external AI models presents unique security considerations:
| Risk Vector | Built-in Protection Mechanism |
|---|---|
| XSS via Malicious Links | SourceList automatically checks all URLs and blocks non-web protocols (javascript:, data:, vbscript:). Only http: and https: are permitted. |
| HTML Injection | Zero usage of dangerouslySetInnerHTML. CodeBlock uses a tokenized syntax highlighter that escapes raw strings into safe React elements. |
| Nested Brace ReDoS / Stack Overflow | scanJSONObjects enforces a maxDepth limit (default 50) when scanning braces. |
| Buffer Memory Exhaustion | Input strings and streaming buffers are capped at maxInputLength (default 1 MB). |
How react-generative-ui compares against alternative architectural choices:
| Feature / Metric | Manual JSON Parsing | @assistant-ui/react-generative-ui |
react-generative-ui |
|---|---|---|---|
| Architecture | Custom regex / ad-hoc | Tightly coupled to assistant-ui runtime |
100% Framework & Runtime Agnostic |
| Component Customization | Built from scratch | Theme overrides | NPM import OR Copy TSX source via CLI |
| Streaming Parser | Hand-rolled buffer logic | Internal framework store | Built-in createStreamingParser |
| LLM Vendor Support | Any | Any (via assistant-ui) | Any (OpenAI, Anthropic, Gemini, Ollama, Custom API) |
| Runtime Validation | Manual | Schema dependent | Built-in Zod support via withSchema |
| Security Guards | Needs manual checks | Internal | Built-in URL sanitizer, depth guards, zero dangerouslySetInnerHTML |
| Dependencies | Custom | assistant-ui stack |
React (zero required external runtime deps) |
Scans rawText for JSON blocks containing "componentName". Surrounding text is converted to Text blocks.
Parses pure JSON array strings or { blocks: [...] } objects into UIBlock[].
Returns { push(chunk: string): UIBlock[], flush(): UIBlock[] } for handling token streams.
Generates a complete system prompt string instructing the LLM on available components and formatting rules.
Generates only the instruction snippet portion of the system prompt.
Pairs a React component with a Zod schema for registration.
<GenerativeRenderer blocks={blocks} registry={registry} fallback={Fallback} debug={boolean} className={string} />
The primary React renderer component.
import type {
UIBlock,
ComponentRegistry,
RegistryEntry,
GenerativeRendererProps,
ParseOptions,
} from 'react-generative-ui';No. It works with any LLM provider (Anthropic Claude, Google Gemini, Ollama, HuggingFace, Groq, or custom local models) as long as the LLM receives the system prompt instruction.
Yes. All components are fully compatible with Next.js (Client Components where interactivity is needed).
No. Zod is optional. If omitted, props passed by the LLM are given directly to your React component.
Only if you use BarChart, LineChart, or PieChart. The other 14 components have zero extra dependencies.
For dedicated topic guides, inspect the /docs directory:
- Getting Started Guide
- Architecture & Deep Dive
- 17 Component Catalog Reference
- Streaming Guide
- Security Model
- API Reference Manual
- CLI Reference
- Frequently Asked Questions
MIT License © Hema Surya
See CHANGELOG.md for version release history.