Solid apps get an extra path the other frameworks don’t: the components are authored in SolidJS, so you can import them straight into your JSX instead of going through the kai-* web components. Both paths ship in the package and mix freely:
- Native SolidJS components (
@kitn.ai/ui/solid) — importChatThread,Message,PromptInput, and the rest directly into your JSX. Full compositional control, tree-shaken alongside your own code. Reach for this in a Solid app. kai-*web components (@kitn.ai/ui/elements) — the drop-in shells when you want batteries included or need to share UI across frameworks.
Install
Section titled “Install”npm install @kitn.ai/uisolid-js is a peer dependency — install it if you haven’t already:
npm install solid-jsThe native components are Tailwind v4 classes over the kit’s --color-* tokens, so the Solid path needs Tailwind in the app. Three lines, all in your CSS entry:
@import "tailwindcss";@import "@kitn.ai/ui/theme.css"; /* the --color-* design tokens */@source "../node_modules/@kitn.ai/ui"; /* let Tailwind see the kit's class names */Add tailwindcss and @tailwindcss/vite to your dev dependencies and put tailwindcss() alongside solidPlugin() in vite.config.ts.
The kai-* elements need none of this: they carry their own compiled styles inside Shadow DOM.
Native components
Section titled “Native components”Compose directly in JSX. Size the layout with flex — ChatContainer fills its box, so give it a flex parent rather than a hard-coded viewport height.
A message is an ordered parts array, not a string: text, reasoning, tool calls, generative-UI cards and file attachments interleaved in the order the model produced them. MessageBody walks that array and renders each kind in place, so render it rather than joining the parts into text.
import { createMemo, createSignal, For, Show } from 'solid-js';import { Button, ChatConfig, ChatContainer, ChatContainerContent, ChatContainerScrollAnchor, Message, MessageBody, PromptInput, PromptInputTextarea, PromptInputActions,} from '@kitn.ai/ui/solid';import type { ChatMessage } from '@kitn.ai/ui/solid';import { appendTextPart } from '@kitn.ai/ui/state';
export function Chat() { const [messages, setMessages] = createSignal<ChatMessage[]>([ { id: '1', role: 'assistant', parts: [{ type: 'text', text: 'How can I help?' }] }, ]); const [input, setInput] = createSignal(''); // The list's KEYS, not the messages. A streaming message is a new object every // delta, so a reference-keyed <For> would rebuild the whole row for each one. const messageKeys = createMemo(() => messages().map((m) => m.id));
const handleSubmit = async () => { const text = input().trim(); if (!text) return;
const history: ChatMessage[] = [ ...messages(), { id: crypto.randomUUID(), role: 'user', parts: [{ type: 'text', text }] }, ]; const replyId = crypto.randomUUID(); setMessages([...history, { id: replyId, role: 'assistant', parts: [] }]); setInput('');
for await (const token of streamFromYourAPI(history)) { // appendTextPart returns a NEW parts array. That reference is the re-render // signal; mutating parts in place renders nothing. setMessages((prev) => prev.map((m) => (m.id === replyId ? { ...m, parts: appendTextPart(m.parts, token) } : m)), ); } };
return ( <div class="flex flex-col h-full"> <ChatConfig proseSize="sm"> <ChatContainer class="h-full"> <ChatContainerContent class="space-y-4 p-4"> <For each={messageKeys()}> {(_id, i) => ( // Read the message through <For>'s index accessor, never through // a captured value: the row outlives the delta that replaced its // object, so every read has to go through msg(). <Show when={messages()[i()]}> {(msg) => ( <Message> <MessageBody parts={msg().parts} isUser={msg().role === 'user'} markdown={msg().role !== 'user'} /> </Message> )} </Show> )} </For> <ChatContainerScrollAnchor /> </ChatContainerContent> </ChatContainer> <PromptInput value={input()} onValueChange={setInput} onSubmit={handleSubmit}> <PromptInputTextarea placeholder="Ask anything..." /> <PromptInputActions> <Button size="sm" disabled={!input()}>Send</Button> </PromptInputActions> </PromptInput> </ChatConfig> </div> );}ChatContainer is transport-agnostic — it owns the conversation UI, you own the request. Render your message list inside ChatContainerContent and stream the assistant reply into your signal in onSubmit.
ChatMessage is the kit’s own public type, the same shape the kai-* elements take and the wire adapter produces from real provider SSE. @kitn.ai/ui/state holds the pure part folds: appendTextPart, appendReasoningPart, upsertToolPart, partsToText.
The action bar
Section titled “The action bar”MessageBody also owns the per-message action row. It holds no state of its own, so the app owns the vote and the transient copied flag:
<MessageBody parts={msg().parts} isUser={msg().role === 'user'} markdown={msg().role !== 'user'} actions={['copy', 'like', 'dislike', 'regenerate']} actionsReveal="hover" activeFeedback={votes()[msg().id]} copied={copiedId() === msg().id} onAction={(id) => handleAction(msg(), id)}/>actions takes the built-in names (copy, like, dislike, regenerate, edit) and/or your own { id, label, icon } descriptors; onAction fires with the name or id.
A whole thread in one component
Section titled “A whole thread in one component”Thread is the message list as a single composable: the scrolling list, per-message rendering, stick-to-bottom with a scroll button, a typing indicator, and an empty state. It is the Solid component behind <kai-thread>, so you get the element’s behavior without adopting the element.
import { Thread } from '@kitn.ai/ui/solid';
<Thread messages={messages()} loading={isLoading()} actionsReveal="hover" onMessageAction={(detail) => handleAction(detail)} empty={<p>Ask me anything.</p>}/>Action buttons come off each message’s own actions field, so onMessageAction is the one handler for the whole list. Pair Thread with your own PromptInput and header. Reach for the <For> + MessageBody composition above when you need to control the row markup itself.
Compose a split layout
Section titled “Compose a split layout”The same building blocks compose into any layout. Use Resizable with ResizablePanel to add a draggable divider between panes — handles are inserted automatically between visible panels.
import { createSignal } from 'solid-js';import { Resizable, ResizablePanel, ConversationList, ChatContainer, ChatContainerContent, PromptInput, PromptInputTextarea } from '@kitn.ai/ui/solid';
function Workspace() { const [activeId, setActiveId] = createSignal<string | undefined>(undefined);
return ( <div class="flex flex-col h-full"> <Resizable orientation="horizontal" onChange={(sizes) => persist(sizes)}> <ResizablePanel defaultSize="25%" minSize="200px"> <ConversationList groups={groups} conversations={conversations()} activeId={activeId()} onSelect={(id) => setActiveId(id)} onNewChat={() => startNewConversation()} /> </ResizablePanel> <ResizablePanel> <ChatContainer class="h-full"> <ChatContainerContent class="p-4"> {/* message list */} </ChatContainerContent> </ChatContainer> <PromptInput onSubmit={handleSubmit}> <PromptInputTextarea placeholder="Ask anything..." /> </PromptInput> </ResizablePanel> </Resizable> </div> );}ResizablePanel accepts defaultSize (px or %) plus optional minSize / maxSize. onChange receives an array of percent sizes; persist it to restore the layout on reload. ConversationList requires both groups (the buckets: Today, Yesterday, …) and conversations. Pass groups={[]} and everything lands in one “Ungrouped” section, or bare rows with compact.
Drop Resizable when you want the handles placed by hand: ResizablePanelGroup + ResizablePanel + an explicit ResizableHandle between them gives you the same layout with control over each divider (handle="grip" | "line" | "none").
Standalone feature components
Section titled “Standalone feature components”Every feature component works standalone — drop them into any part of your UI without adopting a full chat shell.
import { Markdown, Reasoning, ReasoningTrigger, ReasoningContent, Tool, Artifact,} from '@kitn.ai/ui/solid';
<Markdown content={assistantReply} /><Reasoning isStreaming={isStreaming}> <ReasoningTrigger>View reasoning</ReasoningTrigger> <ReasoningContent markdown>{thinkingText}</ReasoningContent></Reasoning><Tool toolPart={toolCall} defaultOpen /><Artifact files={artifactFiles} />Using the web component shell
Section titled “Using the web component shell”When you want a complete chat in one tag, register the kai-* elements and use <kai-chat>. In Solid, pass rich data with prop: (forces a DOM property assignment, not a stringified attribute) and listen for CustomEvents with Solid’s on: namespace.
A message is an ordered parts array, not a string — the same shape as the native components above. ChatMessage is the kit’s own type, so import it rather than hand-rolling a text-only copy — the copy compiles and then hides every other part kind from you.
import '@kitn.ai/ui/elements';import { createSignal } from 'solid-js';import type { ChatMessage } from '@kitn.ai/ui';import { appendTextPart } from '@kitn.ai/ui/state';
function Shell() { const [messages, setMessages] = createSignal<ChatMessage[]>([ { id: '1', role: 'assistant', parts: [{ type: 'text', text: 'Hello! How can I help?' }] }, ]);
const handleSubmit = async (e: CustomEvent<{ value: string }>) => { const userMsg: ChatMessage = { id: crypto.randomUUID(), role: 'user', parts: [{ type: 'text', text: e.detail.value }], }; const history = [...messages(), userMsg]; const aid = crypto.randomUUID(); setMessages([...history, { id: aid, role: 'assistant', parts: [] }]);
for await (const token of streamFromYourAPI(history)) { // appendTextPart returns a NEW parts array and leaves any reasoning, tool // or card parts alone. Replacing `parts` wholesale would drop them. setMessages((prev) => prev.map((m) => (m.id === aid ? { ...m, parts: appendTextPart(m.parts, token) } : m)), ); } };
return ( <div style={{ display: 'flex', 'flex-direction': 'column', height: '100dvh' }}> <kai-chat prop:messages={messages()} on:kai-submit={handleSubmit} style={{ flex: 1, 'min-height': 0 }} /> </div> );}Every element, in Solid
Section titled “Every element, in Solid”Nothing in the kai-* catalog is element-only. Each element is a thin facade over Solid components you can import from @kitn.ai/ui/solid and compose yourself, and every public component ships a matching <Name>Props type. This table is generated from the kit’s element metadata at build time, so it is the whole catalog, not a curated slice:
80 registered elements. 78 wrap SolidJS components you can import
from @kitn.ai/ui/solid and compose yourself; the other 2 expose a
function API rather than a component, so their row is blank. Every name below is cross-checked
against the built package's exports.
| Element | SolidJS components |
|---|---|
| kai-agent-card | AgentCard |
| kai-artifact | Artifact |
| kai-attachments | Attachments, Attachment, AttachmentPreview, AttachmentInfo, AttachmentRemove, AttachmentHoverCard, AttachmentHoverCardTrigger, AttachmentHoverCardContent, AttachmentEmpty |
| kai-audio-visualizer | AudioVisualizer |
| kai-avatar | Avatar |
| kai-badge | Badge |
| kai-button | Button |
| kai-card | Card |
| kai-cards | CardFallback |
| kai-chain-of-thought | ChainOfThoughtAccordion |
| kai-chat | ChatThread |
| kai-checkpoint | Checkpoint, CheckpointIcon, CheckpointTrigger |
| kai-choice | ChoiceCard |
| kai-coachmark | Coachmark |
| kai-code-block | CodeBlock, CodeBlockCode |
| kai-command | CommandList |
| kai-compare | ResponseCompare |
| kai-composer | Composer |
| kai-confirm | ConfirmCard |
| kai-context | Context, ContextTrigger, ContextContent, ContextContentHeader, ContextContentBody, ContextContentFooter, ContextInputUsage, ContextOutputUsage, ContextReasoningUsage, ContextCacheUsage |
| kai-conversations | ConversationList, CollapsedRail |
| kai-dialog | Dialog |
| kai-editable-label | EditableLabel |
| kai-embed | Embed |
| kai-empty | Empty, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription, EmptyContent |
| kai-feedback-bar | FeedbackBar |
| kai-file-tree | FileTree |
| kai-file-upload | FileUpload, FileUploadTrigger |
| kai-form | Form |
| kai-hover-card | HoverCardRoot, HoverCardTrigger, HoverCardContent |
| kai-icon | |
| kai-image | Image |
| kai-input | Input |
| kai-kbd | Kbd |
| kai-link-preview | LinkPreview |
| kai-loader | Loader |
| kai-markdown | Markdown |
| kai-menu | Dropdown, DropdownTrigger, DropdownContent, DropdownItem, DropdownSeparator, DropdownLabel, DropdownCheckboxItem, DropdownRadioItem, DropdownSub, DropdownSubTrigger, DropdownSubContent, Kbd |
| kai-message | Message, MessageAvatar, MessageBody |
| kai-model-switcher | ModelSwitcher |
| kai-nav | Nav |
| kai-notice | Notice |
| kai-pane | Pane |
| kai-pane-group | PaneGroup |
| kai-popover | Popover |
| kai-progress-bar | ProgressBar |
| kai-prompt-dock | PromptDock |
| kai-prompt-input | PromptInput, PromptInputTextarea, PromptInputActions, PromptSuggestion, Button, Tooltip, Attachments, Attachment, AttachmentPreview, AttachmentInfo, AttachmentRemove |
| kai-reasoning | Reasoning, ReasoningTrigger, ReasoningContent |
| kai-remote | |
| kai-resizable | ResizableHandle |
| kai-resizable-item | ResizableHandle |
| kai-response-stream | ResponseStream |
| kai-scope-picker | ChatScopePicker |
| kai-screen | Screen |
| kai-scroll-area | ScrollArea |
| kai-scroll-button | Button |
| kai-search | Input, Kbd, Loader |
| kai-segmented | Segmented |
| kai-separator | Separator |
| kai-setting-item | SettingItem |
| kai-settings-group | SettingsGroup |
| kai-skeleton | Skeleton |
| kai-skills | MessageSkills |
| kai-source | Source, SourceTrigger, SourceContent, SourceList |
| kai-sources | Source, SourceTrigger, SourceContent, SourceList |
| kai-status | Status |
| kai-suggestions | PromptSuggestion |
| kai-switch | Switch |
| kai-tabs | Tabs |
| kai-tasks | TasksCard |
| kai-text-shimmer | TextShimmer |
| kai-thinking-bar | ThinkingBar |
| kai-thread | Thread |
| kai-toast-region | ToastRegion |
| kai-tool | Tool |
| kai-tooltip | Tooltip |
| kai-voice-input | VoiceInput |
| kai-voice-output | VoiceOutput |
| kai-workspace | ChatThread, ConversationList, CollapsedRail, ResizablePanelGroup, ResizablePanel, ResizableHandle |
Each element’s own page carries the same mapping plus its full prop and event reference: start at Components.
Which path to choose
Section titled “Which path to choose”Native components (@kitn.ai/ui/solid) | Web components (@kitn.ai/ui/elements) | |
|---|---|---|
| Recommended for | Solid apps | Any framework, or sharing UI across frameworks |
| Bundle | Tree-shaken with your own code | Pre-built, self-contained |
| Composition | Full JSX control | Single element, attribute-driven |
| Signals | Native createSignal | prop: + on: bindings |
Mix freely: native components on one screen, a <kai-chat> shell on another.
See Installation for bundler configuration and CDN options.
Working on the kit itself? Developing the SolidJS primitives — the layered architecture, adding or modifying a component, running the kit from source — is documented in the kit’s Storybook (the Contributing section), which is the development surface for the Solid layer. These docs focus on using AI/UI; Storybook covers building it.