# HTML

Use AI/UI web components in plain HTML and vanilla JavaScript — no framework, no build tool required.

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.

## Install

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

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

```js

```

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

> **tip:** 
Skip npm entirely and load from a CDN. For quick prototypes this works immediately; for production, pin a specific version to avoid breaking changes (the package is pre-1.0).

```html
<script type="module">

</script>
```

## The property / event contract

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 `CustomEvent`s dispatched on the element itself. They do not bubble.

```js
// Arrays and objects — set the property in JavaScript, not as an attribute
el.messages = [{ id: '1', role: 'assistant', parts: [{ type: 'text', text: '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:** assign a **new array**, and replace any item you changed with a **new object**. 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, because these lists key their rows by item identity. Adding, removing and reordering need only the fresh array; editing an existing item needs both.

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

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

## Complete example — `<kai-chat>`

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

```html
<!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">

    await customElements.whenDefined('kai-chat');

    const chat = document.getElementById('chat');

    // Seed an initial assistant message
    chat.messages = [
      {
        id: crypto.randomUUID(),
        role: 'assistant',
        parts: [{ type: 'text', text: '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', parts: [{ type: 'text', 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', parts: [] }];

      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, parts: [{ type: 'text', text: answer }] } : m,
        );
      }

      chat.loading = false;
    });
  </script>
</body>
</html>
```

## Compose individual elements

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

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

  await customElements.whenDefined('kai-conversations');

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

### Add a resizable divider

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.

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

  const resizable = document.querySelector('kai-resizable');
  resizable.addEventListener('kai-change', (e) => {
    localStorage.setItem('panel-sizes', JSON.stringify(e.detail.sizes));
  });
</script>
```

## Standalone display elements

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:

```html
<script type="module">

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

## Key events reference

| Element | Event | `detail` |
|---|---|---|
| `kai-chat` | `kai-submit` | `{ value: string }` |
| `kai-chat` | `kai-model-change` | `{ model: string }` |
| `kai-conversations` | `kai-conversation-select` | `{ id: string }` |
| `kai-conversations` | `kai-new-chat` | — |
| `kai-conversations` | `kai-toggle-sidebar` | — |
| `kai-resizable` | `kai-change` | `{ sizes: string[] }` |

For the full API of any element — all properties, events, and their types — see the individual component page in the [Components](/components/attachments/) reference.

## Theming

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

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