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';
// 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-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 (flat array; the element groups by recency automatically). 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, content, actions?, reasoning?, tools?, attachments? }.
  • kai-prompt-input fires kai-submit with { value, attachments }. Set loading to true while streaming; the send button disables automatically.

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.