How it's built
You don’t need to know any of this to use <kai-chat>. Read it when you want to go deeper: import a building block directly, compose your own layout, or change the kit itself.
Every kai-* element is a thin Shadow-DOM wrapper around a SolidJS component. The interesting parts — the streaming, the auto-resize, the stick-to-bottom scroll, the accessible toggles — live in plain Solid code underneath. The web component is just the last mile that turns that code into a custom element any framework can use.
Four layers
Section titled “Four layers”The source (src/) stacks in four layers, each built on the one below.
1. Headless primitives (src/primitives/) — behavior, no markup. These are Solid hooks and small utilities: useTextStream (token-by-token streaming), useStickToBottom (auto-scroll that yields when the reader scrolls up), useAutoResize (textarea growth), useVoiceRecorder, plus the generative-UI card contract. They hold logic you’d otherwise rewrite in every chat app.
2. UI primitives (src/ui/) — small, accessible building blocks styled entirely through the design tokens: Button, Avatar, Switch, Tooltip, HoverCard, Dropdown, Resizable, ScrollArea, Separator. They’re keyboard-operable, theme-aware, and know nothing about chat.
3. Feature components (src/components/) — the AI surface, assembled from the layers below: ChatContainer, Message, PromptInput, Markdown, Reasoning, Tool, Artifact, ConversationList, the generative-UI cards, and the rest. This is where “a chat message with copy actions” or “a streaming reasoning panel” actually takes shape.
4. The element facade (src/elements/) — each feature component wrapped as a kai-* custom element. This layer adds Shadow DOM, the design-token stylesheet, prop/attribute handling, and kai-* CustomEvents. It’s deliberately thin: most files are a few lines mapping props and events onto the component underneath.
src/primitives/ → behavior (useTextStream, useStickToBottom, card contract)src/ui/ → accessible building blocks (Button, Switch, Resizable)src/components/ → AI feature components (ChatContainer, Message, PromptInput)src/elements/ → kai-* custom elements (the Shadow-DOM facade)The package exposes two of these layers directly. @kitn.ai/ui (the . export) gives you layers 1–3 as Solid source; @kitn.ai/ui/elements gives you layer 4, the registered kai-* elements. Working with the components directly covers the first; the rest of the docs cover the second.
Why Solid behind a custom element
Section titled “Why Solid behind a custom element”Solid compiles to small, fast DOM updates with no virtual DOM — a good fit for streaming text and frequently-updating chat state. Wrapping each component in a custom element then buys framework independence: the same compiled element drops into React, Vue, Svelte, Angular, or plain HTML, with its styles sealed inside Shadow DOM so neither side can leak into the other.
So the architecture gives you both. Inside a Solid app you can skip the wrapper and compose the components directly. Anywhere else, you get a self-contained element with no Solid runtime to think about.
The facade, concretely
Section titled “The facade, concretely”<kai-switch> is a good small example — the whole element is the wrapper around the Switch UI primitive:
import { defineWebComponent } from './define';import { Switch } from '../ui/switch';
defineWebComponent('kai-switch', { checked: undefined, disabled: undefined, label: undefined,}, (props, { dispatch, flag }) => ( <Switch defaultChecked={flag('checked')} disabled={flag('disabled')} label={props.label} onChange={(checked) => dispatch('kai-change', { checked })} />));defineWebComponent (src/elements/define.tsx) does the heavy lifting for every element:
- Registers the tag and renders the component into its Shadow DOM.
- Adopts the compiled kit stylesheet into each shadow root (one shared
CSSStyleSheet, not a copy per instance), so Tailwind classes resolve inside. - Adds a
themeprop to every element (light/dark/auto, defaultautofollows the OS) that toggles the dark token scope. - Hands the facade two helpers:
dispatch(type, detail)fires akai-*CustomEventoff the host, andflag(name)resolves a boolean the way HTML authors expect —<el checked>,<el checked="true">, andel.checked = trueall read as on.
That last point matters: a bare boolean attribute parses to undefined, not true, so facades use flag() rather than the raw prop for any on/off prop.
Where to go next
Section titled “Where to go next”- Working with the components directly — import the primitives and feature components into a Solid app.
- Run the kit locally — clone, build, and the Storybook dev loop.
- Create or modify a component — add a primitive and expose it as a
kai-*element.