Skip to content
kitn AI/UI

Vue

Drop kai-* web components directly into Vue 3 templates. Bind arrays and objects with the .prop modifier; listen with @kai-event. No adapter needed.

Terminal window
npm install @kitn.ai/ui

Register the elements before createApp().mount(). Vue stamps tags at mount time, and registration is async, so an import alone is not enough: a .prop binding written before the element upgrades is discarded and the UI renders blank. Mount behind elementsReady, the promise the entry exports.

src/main.ts
import { elementsReady } from '@kitn.ai/ui/elements';
import { createApp } from 'vue';
import App from './App.vue';
// Resolves once every kai-* element is defined.
elementsReady.then(() => createApp(App).mount('#app'));

Tell Vue’s template compiler that kai-* tags are native web components, not Vue components. Without this, you’ll see “Unknown custom element” warnings and .prop bindings may misbehave.

vite.config.ts
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [
vue({
template: {
compilerOptions: {
isCustomElement: (tag) => tag.startsWith('kai-'),
},
},
}),
],
});

Add this reference once to give Volar and the template compiler full type coverage for every kai-* element’s attributes, properties, and events:

env.d.ts
/// <reference types="@kitn.ai/ui/elements" />

The rule for every element: rich data (arrays, objects) goes in as DOM properties; interactions come out as CustomEvents.

What you’re bindingVue syntaxExample
Array or object:prop.prop="value":messages.prop="messages"
String / boolean / number:attr="value" or attr="literal":active-id="activeId"
Event@kai-event-name="handler"@kai-submit="onSubmit"

The .prop modifier forces Vue to set the value as a live DOM property rather than serialising it to an HTML attribute string. Always use it for arrays and objects.

<kai-chat> is transport-agnostic: give it a messages array, handle @kai-submit, and stream your model’s reply back into state. The element owns the UI; you own the request.

A message is an ordered parts array, not a string: text, reasoning, tool calls, generative-UI cards and file attachments, in the order the model produced them. 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.

<script setup lang="ts">
import '@kitn.ai/ui/elements';
import type { ChatMessage } from '@kitn.ai/ui';
import { appendTextPart } from '@kitn.ai/ui/state';
import { ref } from 'vue';
const messages = ref<ChatMessage[]>([
{ id: '1', role: 'assistant', parts: [{ type: 'text', text: 'Hello! How can I help?' }] },
]);
const onSubmit = async (e: CustomEvent<{ value: string }>) => {
const userMsg: ChatMessage = { id: crypto.randomUUID(), role: 'user', parts: [{ type: 'text', text: e.detail.value }] };
const history = [...messages.value, userMsg];
messages.value = history;
const aid = crypto.randomUUID();
messages.value = [...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.
messages.value = messages.value.map((m) =>
m.id === aid ? { ...m, parts: appendTextPart(m.parts, token) } : m,
);
}
};
</script>
<template>
<div style="display: flex; flex-direction: column; height: 100dvh;">
<kai-chat
:messages.prop="messages"
:suggestions.prop="['Summarize this chat', 'Start fresh']"
style="flex: 1; min-height: 0;"
@kai-submit="onSubmit"
/>
</div>
</template>

<kai-chat> is one option, not the only one. Every element composes independently. Here’s a sidebar + thread layout:

<script setup lang="ts">
import '@kitn.ai/ui/elements';
import { ref } from 'vue';
const conversations = ref([
{
id: 'c1',
title: 'First chat',
scope: { type: 'document' },
messageCount: 3,
lastMessageAt: '2026-06-01T12:00:00Z',
updatedAt: '2026-06-01T12:00:00Z',
},
]);
const activeId = ref('c1');
const messages = ref([{ id: '1', role: 'assistant', parts: [{ type: 'text', text: 'Hi!' }] }]);
const onSelect = (e: CustomEvent<{ id: string }>) => {
activeId.value = e.detail.id;
// load messages for the selected conversation
};
const onSubmit = (e: CustomEvent<{ value: string }>) => {
// send message and stream reply
};
</script>
<template>
<div style="display: flex; height: 100dvh;">
<kai-conversations
:conversations.prop="conversations"
:active-id="activeId"
style="width: 280px; flex-shrink: 0;"
@kai-conversation-select="onSelect"
@kai-new-chat="startNewConversation"
/>
<kai-chat
:messages.prop="messages"
style="flex: 1; min-width: 0;"
@kai-submit="onSubmit"
/>
</div>
</template>

You can also drop standalone display elements — <kai-markdown>, <kai-code-block>, <kai-artifact>, <kai-reasoning> — anywhere in your own UI to render rich AI content without adopting the full chat shell.

Each element is styled inside its own Shadow DOM. No CSS import is required for the elements themselves. Pull in @kitn.ai/ui/theme.css only when you want to override design tokens:

import '@kitn.ai/ui/theme.css';

See the Theming guide for available tokens.