Guides
Vercel AI SDK
The recommended ToonUI integration path for React and Next.js apps.
This is the primary integration path for ToonUI documentation.
Use AI SDK for the chat/model loop. Use ToonUI for the UI protocol and rendering.
Ownership map
| Layer | Owns |
|---|---|
@toon-ui/core | active catalog, generated prompt, parser, validator, events, messages |
| AI SDK | streaming, model calls, chat message transport |
@toon-ui/react | rendering and user interaction capture |
| Your app | tools, persistence, auth, business behavior |
Install
pnpm add @toon-ui/core @toon-ui/react ai @ai-sdk/react @ai-sdk/openaiShared component keys
export const toonComponentKeys = ['text', 'card', 'form', 'field', 'button', 'table'] as const;Server route
import { openai } from '@ai-sdk/openai';
import { convertToModelMessages, streamText, type UIMessage } from 'ai';
import { createToonProtocol } from '@toon-ui/core';
import { toonComponentKeys } from '@/lib/toon-components';
const toon = createToonProtocol({ components: toonComponentKeys });
const system = [
toon.prompt,
'You are a product assistant.',
'Use ToonUI when a card, table, form, or button reduces friction.',
'Available tools:',
'- searchProducts(query)',
].join('\n\n');
export async function POST(request: Request) {
const { messages } = (await request.json()) as { messages: UIMessage[] };
const result = streamText({
model: openai('gpt-4o-mini'),
system,
messages: await convertToModelMessages(messages),
});
return result.toUIMessageStreamResponse();
}Client renderer
'use client';
import { useChat } from '@ai-sdk/react';
import { ToonMessage, createToonReactRuntime } from '@toon-ui/react';
import { MessageResponse } from '@/components/ai-elements/message';
const toon = createToonReactRuntime({
components: {
text: TextComponent,
card: CardComponent,
form: FormComponent,
field: FieldComponent,
button: ButtonComponent,
table: TableComponent,
},
});
export function Chat() {
const { messages, sendMessage, setMessages } = useChat();
return messages.map((message) => {
const content = message.parts
.filter((part) => part.type === 'text')
.map((part) => part.text ?? '')
.join('\n\n');
if (message.role !== 'assistant') return <div key={message.id}>{content}</div>;
return (
<ToonMessage
key={message.id}
content={content}
runtime={toon}
renderMarkdown={(markdown) => <MessageResponse>{markdown}</MessageResponse>}
renderError={(error) => <MyError details={error.details} />}
onReply={(payload) => setMessages((current) => [...current, toon.messages.toUIMessage(payload)])}
onSubmit={(payload) => setMessages((current) => [...current, toon.messages.toUIMessage(payload)])}
/>
);
});
}Why interactions go back as messages
ToonUI does not execute business logic.
A button click becomes a structured payload. Your app decides whether that payload should:
- go back to the model
- call a tool
- update local UI state
- hit an API route
- be ignored because of permissions
That boundary is the point.
Use another SDK
If you do not use AI SDK, keep this part:
const toon = createToonProtocol({ components });and this part:
const toon = createToonReactRuntime({ components });Replace only the model transport and message storage.