Drop kai-* web components directly into Vue 3 templates. Bind arrays and objects with the .prop modifier; listen with @kai-event. No adapter needed.
Install
Section titled “Install”npm install @kitn.ai/uiRegister elements
Section titled “Register elements”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.
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'));Configure Vite
Section titled “Configure Vite”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.
import { defineConfig } from 'vite';import vue from '@vitejs/plugin-vue';
export default defineConfig({ plugins: [ vue({ template: { compilerOptions: { isCustomElement: (tag) => tag.startsWith('kai-'), }, }, }), ],});TypeScript support
Section titled “TypeScript support”Add this reference once to give Volar and the template compiler full type coverage for every kai-* element’s attributes, properties, and events:
/// <reference types="@kitn.ai/ui/elements" />Binding rules
Section titled “Binding rules”The rule for every element: rich data (arrays, objects) goes in as DOM properties; interactions come out as CustomEvents.
| What you’re binding | Vue syntax | Example |
|---|---|---|
| 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.
A working chat
Section titled “A working chat”<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>Compose your own layout
Section titled “Compose your own layout”<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.
Theming
Section titled “Theming”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.