ToonUI
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

LayerOwns
@toon-ui/coreactive catalog, generated prompt, parser, validator, events, messages
AI SDKstreaming, model calls, chat message transport
@toon-ui/reactrendering and user interaction capture
Your apptools, persistence, auth, business behavior

Install

pnpm add @toon-ui/core @toon-ui/react ai @ai-sdk/react @ai-sdk/openai

Shared component keys

lib/toon-components.ts
export const toonComponentKeys = ['text', 'card', 'form', 'field', 'button', 'table'] as const;

Server route

app/api/chat/route.ts
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

components/chat.tsx
'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.

On this page