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

### Updating an array

**Assign a new array, and replace any item you changed with a new object.** Both halves matter, and each does a different job:

- The **new array reference** is what tells the element something changed. Setting the same array back is a no-op, even if you replaced an item inside it.
- The **new item object** is what makes the change visible. These lists key their rows by item identity, so an item you mutated in place renders stale even inside a brand-new array.

Adding, removing and reordering need only the fresh array — each of those changes the list of item references, which is what the element diffs. Editing a field inside an item changes nothing about that list, which is why it needs the new object as well.

```js
// Stale: `title` really did change, but the item object did not, so the row never updates.
conversations.value.find((c) => c.id === id)!.title = 'Renamed';
conversations.value = [...conversations.value];

// Renders: a new array, and the one item that changed is a new object.
conversations.value = conversations.value.map((c) => (c.id === id ? { ...c, title: 'Renamed' } : c));
```

> **caution:** 
With a plain `ref`, an in-place mutation re-renders **your** template while the element keeps showing the old value — the state is visibly correct in Vue DevTools and visibly wrong on screen. Nothing looks broken at the point you made the change, which is what makes it hard to spot.

Prefer `shallowRef` for data you hand to an element. It won't fix an in-place mutation, but it stops the rest of your app from papering over one.

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