# Vue

Use kai-* web components in Vue 3 templates with property binding and custom event listeners.

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

```bash
npm install @kitn.ai/ui
```

## 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.

```ts
// src/main.ts

// Resolves once every kai-* element is defined.
elementsReady.then(() => createApp(App).mount('#app'));
```

## 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.

```ts
// vite.config.ts

export default defineConfig({
  plugins: [
    vue({
      template: {
        compilerOptions: {
          isCustomElement: (tag) => tag.startsWith('kai-'),
        },
      },
    }),
  ],
});
```

## 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:

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

## 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

`<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.

```vue
<script setup lang="ts">

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>
```

> **tip:** 
The elements fill their container. Give the parent `display: flex; flex-direction: column` and set `flex: 1; min-height: 0` on `<kai-chat>` rather than hard-coding a height — this keeps the input pinned to the bottom at any viewport size.

## Compose your own layout

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

```vue
<script setup lang="ts">

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

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:

```ts

```

See the [Theming guide](/guides/theming/) for available tokens.
