# Solid

Use AI/UI's native SolidJS components from @kitn.ai/ui for full compositional control, or drop in the kai-* web components for a batteries-included shell.

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.

> **note:** 
Import Solid components from `@kitn.ai/ui/solid`, not from the root `@kitn.ai/ui`. The root entry is what every consumer resolves, including the React / Vue / Svelte / vanilla majority who only ever touch the web components, so it carries the shared layer: the chat components, the types, the state and card helpers. `@kitn.ai/ui/solid` is that entry plus the rest of the Solid catalog (`ChatThread`, `Dialog`, `Popover`, `Input`, `Nav`, `Tabs`, and the other primitives the elements are built from), compiled as its own bundle so a React consumer never pays for it. It is a superset, so one import covers everything.

## Install

```bash
npm install @kitn.ai/ui
```

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

```bash
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:

```css
/* 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 */
```

> **caution:** 
Tailwind v4 only scans your own `src/` by default. Without the `@source` line it strips every kit utility class as unused and the components render unstyled. Point it at wherever `@kitn.ai/ui` resolves from your CSS file.

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.

> **note:** 
The package ships **compiled**. `@kitn.ai/ui/solid` resolves to `dist/solid.js` in the browser and `dist/solid.server.js` under the `node` / `deno` / `worker` conditions; `@kitn.ai/ui` resolves to `dist/index.js` / `dist/index.server.js` the same way. All four are ESM, so your bundler still tree-shakes down to what you import, but it never compiles the kit's TSX. A stock `vite-plugin-solid()` is enough; nothing has to be told to transform `node_modules`.

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

```tsx

  Button,
  ChatConfig,
  ChatContainer,
  ChatContainerContent,
  ChatContainerScrollAnchor,
  Message,
  MessageBody,
  PromptInput,
  PromptInputTextarea,
  PromptInputActions,
} from '@kitn.ai/ui/solid';

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>
                      
                    </Message>
                  )}
                </Show>
              )}
            </For>
            
          </ChatContainerContent>
        </ChatContainer>
        <PromptInput value={input()} onValueChange={setInput} onSubmit={handleSubmit}>
          
          <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](/guides/recipes/wire-adapter/) produces from real provider SSE. `@kitn.ai/ui/state` holds the pure part folds: `appendTextPart`, `appendReasoningPart`, `upsertToolPart`, `partsToText`.

> **caution:** 
`<For>` diffs by object identity, and a streaming message is a new object on every delta: that new reference is what tells Solid to re-render. Key the list on the message objects and each chunk looks like an entirely new list, so the row is torn down and rebuilt. A tool or reasoning panel the reader opens mid-stream then closes itself on the next token.

Key on the ids and read the message back through `<For>`'s index accessor, as above. The keys are plain strings, a delta produces an identical key list, and the row stays put while its content updates.

Not `<Index>` here. Position keying leaves an open panel with the slot rather than the message, so loading older turns in at the top would move every open disclosure onto the wrong message. Position is the right key *inside* a message, which is what `MessageBody` already does: the folds behind `parts` only append or patch in place.

> **caution:** 
`<MessageContent>{msg.parts.map((p) => p.text).join('')}</MessageContent>` compiles and looks fine on a text-only thread, then silently drops every reasoning block, tool call and card the moment a model emits one. `MessageContent` renders one blob of content; `MessageBody` renders the whole message.

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

```tsx
<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

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

```tsx

<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

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.

```tsx

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

Every feature component works standalone — drop them into any part of your UI without adopting a full chat shell.

```tsx

  Markdown, Reasoning, ReasoningTrigger, ReasoningContent, Tool, Artifact,
} from '@kitn.ai/ui/solid';

<Reasoning isStreaming={isStreaming}>
  <ReasoningTrigger>View reasoning</ReasoningTrigger>
  <ReasoningContent markdown>{thinkingText}</ReasoningContent>
</Reasoning>

```

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

```tsx

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

> **tip:** 
Without `prop:`, Solid serializes objects to strings — breaking array props. Use `prop:messages` (not `messages`) for any prop that takes an array or object.

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

Each element's own page carries the same mapping plus its full prop and event reference: start at [Components](/components/chat/).

## 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](/guides/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.
