Skip to content
kitn AI/UI

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.

Terminal window
npm install @kitn.ai/ui

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 file
import { 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.

<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.

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>
);
}

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>

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 />

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 eventReact prop
kai-submitonSubmit
kai-value-changeonValueChange
kai-message-actiononMessageAction
kai-model-changeonModelChange
kai-conversation-selectonConversationSelect
kai-suggestion-clickonSuggestionClick

Every wrapper also accepts className, style, id, theme ('light' | 'dark' | 'auto'), and children (for slotted content).

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';

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' }} />;
}