React
@kitn.ai/ui/react exports a typed React wrapper for every kai-* web component. Pass data as props, handle events with onX handlers — no refs, no manual addEventListener, no stringified objects.
Install
Section titled “Install”npm install @kitn.ai/uiSet up
Section titled “Set up”Register the web components once at your app entry point, then import the wrappers you need anywhere:
// main.tsx (or wherever you mount your app)import '@kitn.ai/ui/elements'; // registers all kai-* elements globally// Any component fileimport { Chat, Markdown, Reasoning } from '@kitn.ai/ui/react';Component names are the PascalCase of the tag name: kai-chat → Chat, kai-prompt-input → PromptInput, kai-chain-of-thought → ChainOfThought.
The all-in-one shell
Section titled “The all-in-one shell”<Chat> renders a complete thread — message list, prompt input, suggestions, and header chrome — in a single element. It is transport-agnostic: pass a messages array, handle onSubmit, and stream the reply back into state.
import '@kitn.ai/ui/elements';import { Chat } from '@kitn.ai/ui/react';import { useState } from 'react';
type Message = { id: string; role: 'user' | 'assistant'; content: string;};
export function App() { const [messages, setMessages] = useState<Message[]>([ { id: '1', role: 'assistant', content: 'How can I help?' }, ]);
const handleSubmit = async (e: CustomEvent<{ value: string }>) => { const userMsg: Message = { id: crypto.randomUUID(), role: 'user', content: e.detail.value, }; const history = [...messages, userMsg]; setMessages(history);
const aid = crypto.randomUUID(); setMessages([...history, { id: aid, role: 'assistant', content: '' }]);
let answer = ''; for await (const token of streamFromYourAPI(history)) { answer += token; setMessages((prev) => prev.map((m) => (m.id === aid ? { ...m, content: answer } : m)), ); } };
return ( <div style={{ display: 'flex', flexDirection: 'column', height: '100dvh' }}> <Chat messages={messages} suggestions={['Summarize the chat', 'Start fresh']} onSubmit={handleSubmit} style={{ flex: 1, minHeight: 0 }} /> </div> );}<Chat> fills its container. Use flex: 1 (not a hard-coded height) so it adapts to your layout.
Compose individual elements
Section titled “Compose individual elements”Every element has its own wrapper, so you can assemble your own layout. This example pairs a <Conversations> sidebar with a <Chat> thread:
import '@kitn.ai/ui/elements';import { Chat, Conversations } from '@kitn.ai/ui/react';import { useState } from 'react';
export function Workspace() { const [activeId, setActiveId] = useState(myConversations[0]?.id); const [messages, setMessages] = useState(loadMessages(activeId));
return ( <div style={{ display: 'flex', height: '100dvh' }}> <Conversations conversations={myConversations} activeId={activeId} onConversationSelect={(e) => { setActiveId(e.detail.id); setMessages(loadMessages(e.detail.id)); }} onNewChat={() => startNewConversation()} style={{ width: 280, flexShrink: 0 }} /> <Chat messages={messages} onSubmit={(e) => sendMessage(e.detail.value)} style={{ flex: 1, minWidth: 0 }} /> </div> );}Resizable panels
Section titled “Resizable panels”Wrap panels in <Resizable> with one <ResizableItem> each to add a draggable divider. Each item takes a size (px or %) and optional min/max. Listen for onChange to persist the layout.
import { Chat, Conversations, Resizable, ResizableItem } from '@kitn.ai/ui/react';
<Resizable orientation="horizontal" style={{ flex: 1, minHeight: 0 }}> <ResizableItem size="25%" min="200px"> <Conversations conversations={conversations} activeId={activeId} onConversationSelect={handleSelect} /> </ResizableItem> <ResizableItem> <Chat messages={messages} onSubmit={handleSubmit} /> </ResizableItem></Resizable>Standalone display elements
Section titled “Standalone display elements”Drop individual elements anywhere in your UI without adopting a full chat shell — <Markdown>, <CodeBlock>, <Reasoning>, <Tool>, <Artifact> all work standalone:
import { Markdown, Reasoning, Tool } from '@kitn.ai/ui/react';
<Markdown content={assistantReply} proseSize="sm" /><Reasoning text={thinkingText} streaming={isStreaming} /><Tool tool={toolCall} open />Props and events
Section titled “Props and events”Rich data goes in as props; interactions come out as events. The wrappers assign arrays and objects directly as DOM properties (not stringified), and surface CustomEvents as onX handlers.
Event prop names strip the kai- prefix and PascalCase each hyphen-segment:
| DOM event | React prop |
|---|---|
kai-submit | onSubmit |
kai-value-change | onValueChange |
kai-message-action | onMessageAction |
kai-model-change | onModelChange |
kai-conversation-select | onConversationSelect |
kai-suggestion-click | onSuggestionClick |
Every wrapper also accepts className, style, id, theme ('light' | 'dark' | 'auto'), and children (for slotted content).
All available wrappers
Section titled “All available wrappers”import { Artifact, Attachments, Card, Cards, ChainOfThought, Chat, Checkpoint, Choice, CodeBlock, Confirm, Context, Conversations, Embed, Empty, FeedbackBar, FileTree, FileUpload, Form, Image, LinkPreview, Loader, Markdown, Message, ModelSwitcher, PromptInput, Reasoning, Remote, Resizable, ResizableItem, ResponseStream, ScopePicker, ScrollButton, Skills, Source, Sources, Suggestions, Tasks, TextShimmer, ThinkingBar, Tool, VoiceInput, Workspace,} from '@kitn.ai/ui/react';Raw web component usage
Section titled “Raw web component usage”Prefer to use the kai-* element directly — for example with React 19’s improved custom-element support? Use a ref to set object props and wire events manually:
import { useEffect, useRef, useState } from 'react';import '@kitn.ai/ui/elements';
export function RawChat() { const ref = useRef<HTMLElement>(null); const [messages, setMessages] = useState([ { id: '1', role: 'assistant', content: 'Hello!' }, ]);
useEffect(() => { const el = ref.current; if (!el) return; // Arrays/objects must be assigned as DOM properties, not attributes. (el as any).messages = messages; }, [messages]);
useEffect(() => { const el = ref.current; if (!el) return; const onSubmit = (e: Event) => { const { value } = (e as CustomEvent<{ value: string }>).detail; setMessages((prev) => [ ...prev, { id: crypto.randomUUID(), role: 'user', content: value }, ]); }; el.addEventListener('kai-submit', onSubmit); return () => el.removeEventListener('kai-submit', onSubmit); }, []);
return <kai-chat ref={ref} style={{ display: 'block', height: '100dvh' }} />;}