Skip to content
kitn AI/UI

Solid

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) — import ChatThread, 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.
Terminal window
npm install @kitn.ai/ui

solid-js is a peer dependency — install it if you haven’t already:

Terminal window
npm install solid-js

The 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:

src/styles.css
@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.

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.

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.

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.

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

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

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

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.

Native components (@kitn.ai/ui/solid)Web components (@kitn.ai/ui/elements)
Recommended forSolid appsAny framework, or sharing UI across frameworks
BundleTree-shaken with your own codePre-built, self-contained
CompositionFull JSX controlSingle element, attribute-driven
SignalsNative createSignalprop: + 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.