@kitn.ai/chat
Version:
Framework-agnostic, Shadow-DOM web components for building AI chat interfaces — works in React, Vue, Angular, Svelte, or plain HTML. Authored in SolidJS.
270 lines (190 loc) • 9.23 kB
text/mdx
import { Meta } from '@storybook/addon-docs/blocks';
<Meta title="Docs/Composition & Child Elements" />
# Composition & Child Elements
Seven collection elements in the kit accept two input paths — **data** (a JS property array) and **composition** (declarative child markup). Both produce identical rendered output and can be mixed on the same element.
## Two ways to populate a collection
### Data — the JS property array
Set the element's array property from JavaScript. The parent re-renders reactively whenever the reference changes.
```html
<kc-suggestions id="s"></kc-suggestions>
<script type="module">
import '@kitn.ai/chat/elements';
document.getElementById('s').suggestions = [
{ label: 'Summarise this', value: 'summarise' },
{ label: 'Translate to French', value: 'translate' },
];
</script>
```
### Composition — declarative child elements
Place `<kc-*>` child elements directly in markup. They live in the light DOM but are invisible — the parent's Shadow DOM has no `<slot>` for them, so it reads them via `querySelectorAll` and a `MutationObserver` instead. No JavaScript required.
```html
<kc-suggestions>
<kc-suggestion value="summarise">Summarise this</kc-suggestion>
<kc-suggestion value="translate">Translate to French</kc-suggestion>
</kc-suggestions>
<script type="module">
import '@kitn.ai/chat/elements';
</script>
```
### Merge order
When both are provided, **property items render first** and child elements are appended after. There is no conflict — they simply concatenate.
---
## Choosing the right path
| Situation | Reach for |
|-----------|-----------|
| Data arrives from a fetch, a stream, or application state | **Data (property)** |
| Content is model-driven, user-specific, or changes at runtime | **Data (property)** |
| You're working in React, Vue, Svelte, or Angular with a component model | **Data (property)** |
| Content is static and known at build time | **Composition (children)** |
| You're authoring a CMS template, server-rendered HTML, or a no-build page | **Composition (children)** |
| You want zero JavaScript for the initial population | **Composition (children)** |
---
## Child element reference
Each row below is verified against the element's source in `src/elements/`.
### `<kc-action>`
**Parents:** `<kc-message>` (action bar → `kc-message-action`) and `<kc-prompt-input>` (custom toolbar buttons → `kc-toolbar-action`)
| Attribute | Maps to | Notes |
|-----------|---------|-------|
| `id` | `CustomAction.id` | Falls back to the `action` attribute when `id` is absent |
| `action` | `CustomAction.id` | Secondary fallback when `id` is also absent |
| `icon` | `CustomAction.icon` | Optional icon name |
| `tooltip` | `CustomAction.tooltip` | Optional tooltip string |
| *(text content)* | `CustomAction.label` | Falls back to the `label` attribute, then `id` |
**Event fired by parent:** `kc-message-action` → `{ messageId: string, action: string }`
```html
<kc-message>
<kc-action id="copy" icon="copy" tooltip="Copy to clipboard">Copy</kc-action>
<kc-action id="retry" icon="refresh">Retry</kc-action>
</kc-message>
```
---
### `<kc-suggestion>`
**Parent:** `<kc-suggestions>`
| Attribute | Maps to | Notes |
|-----------|---------|-------|
| `value` | `Item.value` | Falls back to text content when absent |
| *(text content)* | `Item.label` | The chip label |
**Event fired by parent:** `kc-select` → `{ value: string }`
```html
<kc-suggestions>
<kc-suggestion value="summarise">Summarise this</kc-suggestion>
<kc-suggestion value="translate">Translate to French</kc-suggestion>
</kc-suggestions>
```
---
### `<kc-source>`
**Parent:** `<kc-sources>`
| Attribute | Maps to | Notes |
|-----------|---------|-------|
| `href` | `SourceItem.href` | Required — the citation URL |
| `label` | `SourceItem.label` | Trigger label (defaults to the domain) |
| `headline` | `SourceItem.title` | Hover-card headline. Named `headline` (not `title`) because `title` is a reserved HTML global attribute |
| `description` | `SourceItem.description` | Hover-card body text |
| `show-favicon` | `SourceItem.showFavicon` | Boolean — bare attribute or `show-favicon="true"` |
| *(text content)* | *(not read by `kc-sources`)* | Text content on a child `<kc-source>` is not mapped by the parent; use `label`/`headline`/`description` attributes instead |
**No interaction event** — sources open their `href` as links.
```html
<kc-sources>
<kc-source
href="https://example.com/page"
label="Example"
headline="Example Domain"
description="An illustrative domain used in documentation."
show-favicon
></kc-source>
</kc-sources>
```
> **Note:** `<kc-source>` also works as a **standalone** element — its own `href`, `label`, `headline`, `description`, and `show-favicon` props drive a single citation chip with a hover-card. The child-element role above is its use *inside* `<kc-sources>`.
---
### `<kc-conversation>`
**Parent:** `<kc-conversations>`
| Attribute | Maps to | Notes |
|-----------|---------|-------|
| `id` | `ConversationSummary.id` | Required — stable conversation identifier |
| `group-id` | `ConversationSummary.groupId` | Optional group bucket (e.g. "today", "yesterday") |
| *(text content)* | `ConversationSummary.title` | The conversation title shown in the list |
Required fields not expressible as HTML attributes (`scope`, `messageCount`, `lastMessageAt`, `updatedAt`) receive safe defaults so the list item is fully functional with just `id` and text.
**Events fired by parent:** `kc-conversation-select` → `{ id: string }`, `kc-new-chat` → `{}`, `kc-toggle-sidebar` → `{}`
```html
<kc-conversations>
<kc-conversation id="conv-1" group-id="today">Morning standup recap</kc-conversation>
<kc-conversation id="conv-2" group-id="yesterday">API design review</kc-conversation>
</kc-conversations>
```
---
### `<kc-step>`
**Parent:** `<kc-chain-of-thought>`
| Attribute | Maps to | Notes |
|-----------|---------|-------|
| `label` | `Step.label` | The always-visible step heading (required) |
| *(text content)* | `Step.content` | Optional expandable detail; empty string is treated as `undefined` (no expand affordance) |
**No interaction event** — steps expand/collapse internally.
```html
<kc-chain-of-thought>
<kc-step label="Understand the request">The user wants a concise summary.</kc-step>
<kc-step label="Draft a response">Condense the key points into three sentences.</kc-step>
<kc-step label="Review"></kc-step>
</kc-chain-of-thought>
```
---
### `<kc-model>`
**Parent:** `<kc-model-switcher>`
| Attribute | Maps to | Notes |
|-----------|---------|-------|
| `id` | `ModelOption.id` | Required — the model identifier passed in `kc-model-change` |
| `provider` | `ModelOption.provider` | Optional provider name shown in the switcher UI |
| *(text content)* | `ModelOption.name` | Human-readable model name |
**Event fired by parent:** `kc-model-change` → `{ modelId: string }`
The switcher only renders when more than one model is provided.
```html
<kc-model-switcher>
<kc-model id="gpt-4o" provider="OpenAI">GPT-4o</kc-model>
<kc-model id="gpt-4o-mini" provider="OpenAI">GPT-4o mini</kc-model>
<kc-model id="claude-3-7-sonnet" provider="Anthropic">Claude 3.7 Sonnet</kc-model>
</kc-model-switcher>
```
---
### `<kc-skill>`
**Parent:** `<kc-skills>`
| Attribute | Maps to | Notes |
|-----------|---------|-------|
| `id` | `Skill.id` | Falls back to text content when absent |
| *(text content)* | `Skill.name` | The badge label |
**No interaction event** — skills badges are display-only.
```html
<kc-skills>
<kc-skill id="web-search">Web Search</kc-skill>
<kc-skill id="code">Code</kc-skill>
</kc-skills>
```
---
## Side-by-side comparison
The same set of suggestions rendered both ways — identical output, different authoring model.
**Data path:**
```html
<kc-suggestions id="s1"></kc-suggestions>
<script type="module">
import '@kitn.ai/chat/elements';
document.getElementById('s1').suggestions = [
{ label: 'Explain quantum computing', value: 'explain-quantum' },
{ label: 'Write a haiku', value: 'haiku' },
{ label: 'Summarise in bullet points', value: 'summarise' },
];
</script>
```
**Composition path:**
```html
<kc-suggestions>
<kc-suggestion value="explain-quantum">Explain quantum computing</kc-suggestion>
<kc-suggestion value="haiku">Write a haiku</kc-suggestion>
<kc-suggestion value="summarise">Summarise in bullet points</kc-suggestion>
</kc-suggestions>
<script type="module">
import '@kitn.ai/chat/elements';
</script>
```
Both fire `kc-select` with the same `value` string. The only difference is where the data lives — in application state or in the HTML.
---
## Customisation
Child elements are **attribute- and text-driven data carriers**. Their visual appearance is entirely controlled by the parent element's props and the kit's design tokens (`--kc-*`, `--color-*`). There is no CSS to pierce through Shadow DOM boundaries and no per-child styling surface — reach for the parent's documented attributes and tokens instead.
See **[Theming](?path=/docs/theming-overview--docs)** for the full token reference.