Compose your own shell
When <kai-chat> doesn’t fit your layout — you need a third panel, a draggable sidebar, or a bespoke header — compose the shell from its leaf components. You own the data flow; the components handle the rendering.
How it works
Section titled “How it works”Three real kai-* web components inside a <kai-resizable> layout:
<kai-resizable orientation="horizontal" style="display:block;height:560px">
<!-- Sidebar: conversation list --> <kai-resizable-item size="22%" min="160px" max="40%"> <kai-conversations id="list" style="display:block;height:100%"></kai-conversations> </kai-resizable-item>
<!-- Main: scrolling thread + composer --> <kai-resizable-item> <div class="chat-col"> <div id="thread" class="messages"></div> <kai-prompt-input id="input" placeholder="Message the assistant…"></kai-prompt-input> </div> </kai-resizable-item>
</kai-resizable>
<script type="module"> import '@kitn.ai/ui/elements';
// Assign arrays and objects in JavaScript — they can't be HTML attributes. const list = document.getElementById('list'); list.conversations = [ { id: 'c1', title: 'Web component architecture', scope: { type: 'document' }, messageCount: 12, lastMessageAt: '2026-06-16T10:00:00Z', updatedAt: '2026-06-16T10:00:00Z' }, ]; list.activeId = 'c1';
// Build a <kai-message> per turn — you control the thread array. const thread = document.getElementById('thread'); let msgs = [ { id: 'u1', role: 'user', content: 'Hello!' }, { id: 'a1', role: 'assistant', content: 'Hi — how can I help?', actions: ['copy', 'like', 'dislike'] }, ];
const render = () => { thread.innerHTML = ''; for (const m of msgs) { const el = document.createElement('kai-message'); el.message = m; // the full ChatMessage object thread.append(el); } }; render();
// Conversation switch list.addEventListener('kai-conversation-select', (e) => { list.activeId = e.detail.id; // load the thread for the selected conversation… });
// Submit: append user turn, then stream the assistant reply const input = document.getElementById('input'); input.addEventListener('kai-submit', async (e) => { const prompt = e.detail.value; const aId = crypto.randomUUID();
msgs = [ ...msgs, { id: crypto.randomUUID(), role: 'user', content: prompt }, { id: aId, role: 'assistant', content: '' }, ]; input.loading = true; render();
for await (const token of streamFromYourModel(prompt)) { msgs = msgs.map((m) => m.id === aId ? { ...m, content: m.content + token } : m ); render(); } input.loading = false; });</script>Key points:
kai-resizablelays out panels horizontally (or vertically withorientation="vertical"). Eachkai-resizable-itemtakes asize,min, andmax— all as HTML attributes (scalar values).kai-conversationsaccepts aconversationsproperty (flat array; the element groups by recency automatically). Listen forkai-conversation-selectto updateactiveId.- Create one
<kai-message>per turn and set itsmessageproperty to the fullChatMessageobject —{ id, role, content, actions?, reasoning?, tools?, attachments? }. kai-prompt-inputfireskai-submitwith{ value, attachments }. Setloadingtotruewhile streaming; the send button disables automatically.
Adding a third panel
Section titled “Adding a third panel”Insert another <kai-resizable-item> for anything — a <kai-artifact> preview, a canvas, or a debug inspector:
<kai-resizable-item size="32%" min="240px"> <kai-artifact id="preview" style="display:block;height:100%"></kai-artifact></kai-resizable-item>Toggle it with the hidden boolean attribute/property; the remaining panels reflow automatically.
Next steps
Section titled “Next steps”- Drop-in chat — the 90% path: one
<kai-chat>element with no manual wiring. - Streaming recipe — wire the
kai-submitloop to a real SSE stream. kai-resizablereference —orientation,maximizedIndex,kai-change.kai-conversationsreference —groupsvsconversations,activeId, events.kai-messagereference —messageshape,actionsReveal,proseSize.kai-prompt-inputreference —loading,stoppable,slashCommands,kai-toolbar-action.