Skip to content
kitn AI/UI

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.

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';
await customElements.whenDefined('kai-conversations');
// 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', parts: [{ type: 'text', text: 'Hello!' }] },
{ id: 'a1', role: 'assistant', parts: [{ type: 'text', text: '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', parts: [{ type: 'text', text: prompt }] },
{ id: aId, role: 'assistant', parts: [] },
];
input.loading = true;
render();
let answer = '';
for await (const token of streamFromYourModel(prompt)) {
answer += token;
msgs = msgs.map((m) =>
m.id === aId ? { ...m, parts: [{ type: 'text', text: answer }] } : m
);
render();
}
input.loading = false;
});
</script>

Key points:

  • kai-resizable lays out panels horizontally (or vertically with orientation="vertical"). Each kai-resizable-item takes a size, min, and max — all as HTML attributes (scalar values).
  • kai-conversations accepts a conversations property (the rows) and an optional groups property (the section headers, matched against each row’s groupId). With no groups, the whole list renders in one Ungrouped section. Listen for kai-conversation-select to update activeId.
  • Create one <kai-message> per turn and set its message property to the full ChatMessage object — { id, role, parts, actions?, avatar?, feedback? }, where parts is an ordered array of text / reasoning / tool / card / source / file parts.
  • kai-prompt-input fires kai-submit with { value, doc, entities, attachments }value is the flattened text, doc and entities the structured pills. Set loading to true while streaming and the send button disables; add stoppable to swap it for a Stop button that fires kai-stop.

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.