Skip to content
kitn AI/UI

HTML

Drop <kai-*> web components into any HTML page with a single import. No framework, no build configuration — the elements register themselves globally and work in every modern browser.

Terminal window
npm install @kitn.ai/ui

Register all web components once with a side-effect import near your app entry:

import '@kitn.ai/ui/elements';

Every <kai-*> tag is then available globally — drop them anywhere in your HTML.

Every kai-* web component follows the same two rules:

  • Rich data (arrays, objects) → JS properties. Set them in a <script> block, not as HTML attributes. Attributes only work for scalars (strings, booleans, numbers).
  • Interactions → addEventListener. All events are CustomEvents dispatched on the element itself. They do not bubble.
// Arrays and objects — set the property in JavaScript, not as an attribute
el.messages = [{ id: '1', role: 'assistant', content: 'Hi' }];
// Scalars — fine as attributes or properties
el.setAttribute('theme', 'dark');
// Events — always addEventListener; they do not bubble
el.addEventListener('kai-submit', (e) => console.log(e.detail.value));

Reactivity: always assign a new array (and a new object for any message you change). Mutating an existing array or object in place will not trigger a re-render.

<kai-chat> is the batteries-included shell: give it a messages array, handle the kai-submit event, and stream your model’s reply back. You own the request; the element owns the UI.

<!DOCTYPE html>
<html>
<head>
<style>
html, body { margin: 0; height: 100%; }
.app { display: flex; flex-direction: column; height: 100dvh; }
</style>
</head>
<body>
<div class="app">
<kai-chat id="chat" style="flex: 1; min-height: 0;"></kai-chat>
</div>
<script type="module">
import '@kitn.ai/ui/elements';
const chat = document.getElementById('chat');
// Seed an initial assistant message
chat.messages = [
{
id: crypto.randomUUID(),
role: 'assistant',
content: 'Hello! How can I help?',
actions: ['copy', 'like', 'dislike'],
},
];
chat.suggestions = ['Summarize the chat', 'Start fresh'];
chat.addEventListener('kai-submit', async (e) => {
const text = e.detail.value;
// Append the user message — always assign a new array
const history = [
...chat.messages,
{ id: crypto.randomUUID(), role: 'user', content: text },
];
chat.messages = history;
chat.loading = true;
// Create an empty assistant placeholder to stream into
const aid = crypto.randomUUID();
chat.messages = [...history, { id: aid, role: 'assistant', content: '' }];
let answer = '';
for await (const token of streamFromYourAPI(history)) {
answer += token;
// Replace only the placeholder — every other message stays the same
chat.messages = chat.messages.map((m) =>
m.id === aid ? { ...m, content: answer } : m,
);
}
chat.loading = false;
});
</script>
</body>
</html>

<kai-chat> is one option, not the only one. Every element can be placed independently. Here’s a sidebar + thread layout using <kai-conversations> and <kai-chat>:

<style>
html, body { margin: 0; height: 100%; }
.workspace { display: flex; height: 100dvh; }
</style>
<div class="workspace">
<kai-conversations id="sidebar" style="width: 280px; flex-shrink: 0;"></kai-conversations>
<kai-chat id="chat" style="flex: 1; min-width: 0;"></kai-chat>
</div>
<script type="module">
import '@kitn.ai/ui/elements';
const sidebar = document.getElementById('sidebar');
const chat = document.getElementById('chat');
sidebar.conversations = myConversations;
chat.messages = loadMessages(myConversations[0]?.id);
sidebar.addEventListener('kai-conversation-select', (e) => {
chat.messages = loadMessages(e.detail.id);
});
sidebar.addEventListener('kai-new-chat', () => startNewConversation());
chat.addEventListener('kai-submit', (e) => sendMessage(e.detail.value));
</script>

Wrap panels in <kai-resizable> with one <kai-resizable-item> each — drag handles are inserted automatically. Each item accepts a size (px or %) and optional min/max. Listen for kai-change to persist the layout.

<style>
html, body { margin: 0; height: 100%; }
.app { display: flex; flex-direction: column; height: 100dvh; }
</style>
<div class="app">
<kai-resizable orientation="horizontal" style="flex: 1; min-height: 0;">
<kai-resizable-item size="25%" min="200px">
<kai-conversations id="sidebar"></kai-conversations>
</kai-resizable-item>
<kai-resizable-item>
<kai-chat id="chat"></kai-chat>
</kai-resizable-item>
</kai-resizable>
</div>
<script type="module">
import '@kitn.ai/ui/elements';
const resizable = document.querySelector('kai-resizable');
resizable.addEventListener('kai-change', (e) => {
localStorage.setItem('panel-sizes', JSON.stringify(e.detail.sizes));
});
</script>

You can drop any single element into an existing page without adopting the full chat shell. <kai-markdown>, <kai-code-block>, and <kai-artifact> are common standalone picks for rendering AI-generated content:

<script type="module">
import '@kitn.ai/ui/elements';
</script>
<kai-markdown content="## Result\n\nHere is your **summary**."></kai-markdown>
<kai-code-block code="const greet = (name) => `Hello, ${name}!`;" language="js"></kai-code-block>

Each element fills its container and is controlled entirely through properties and events.

ElementEventdetail
kai-chatkai-submit{ value: string }
kai-chatkai-model-change{ model: string }
kai-conversationskai-conversation-select{ id: string }
kai-conversationskai-new-chat
kai-conversationskai-toggle-sidebar
kai-resizablekai-change{ sizes: string[] }

For the full API of any element — all properties, events, and their types — see the individual component page in the Components reference.

By default each element is styled inside its own Shadow DOM and needs no external CSS. To override design tokens (colors, radii, spacing), load theme.css:

<!-- bundler -->
<link rel="stylesheet" href="./node_modules/@kitn.ai/ui/theme.css" />
<!-- CDN -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@kitn.ai/ui/theme.css" />

Then override any --color-* or --radius-* custom properties on :root or a scoping selector.