Angular
Angular binds to DOM properties natively with [prop]="value" and listens to CustomEvents with (kai-event)="handler($event)". The kai-* web components slot into Angular templates directly — no wrapper library, no adapter.
Install
Section titled “Install”npm install @kitn.ai/uiRegister the elements
Section titled “Register the elements”Import the element bundle once as a side-effect in main.ts, before bootstrapApplication. This registers every kai-* web component globally.
import '@kitn.ai/ui/elements';import { bootstrapApplication } from '@angular/platform-browser';import { AppComponent } from './app/app.component';import { appConfig } from './app/app.config';
bootstrapApplication(AppComponent, appConfig);Then add CUSTOM_ELEMENTS_SCHEMA to every standalone component whose template uses kai-* tags. Without it, the Angular compiler rejects the unknown element names at build time.
import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
@Component({ selector: 'app-root', standalone: true, templateUrl: './app.component.html', schemas: [CUSTOM_ELEMENTS_SCHEMA],})export class AppComponent {}Quick start — the all-in-one shell
Section titled “Quick start — the all-in-one shell”<kai-chat> is transport-agnostic: pass a messages array, handle (kai-submit), and stream the reply back into state. The component owns the UI; you own the request.
import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
type Message = { id: string; role: 'user' | 'assistant'; content: string };
@Component({ selector: 'app-root', standalone: true, templateUrl: './app.component.html', schemas: [CUSTOM_ELEMENTS_SCHEMA],})export class AppComponent { messages: Message[] = [ { id: '1', role: 'assistant', content: 'Hello! How can I help?' }, ];
async onSubmit(e: Event) { const { value } = (e as CustomEvent<{ value: string }>).detail; // Reassign a new array so Angular change detection picks it up. const history: Message[] = [ ...this.messages, { id: crypto.randomUUID(), role: 'user', content: value }, ]; this.messages = history;
const aid = crypto.randomUUID(); this.messages = [...history, { id: aid, role: 'assistant', content: '' }];
let answer = ''; for await (const token of streamFromYourAPI(history)) { answer += token; this.messages = this.messages.map((m) => m.id === aid ? { ...m, content: answer } : m ); } }}<!-- kai-chat fills its container — use flex so it grows with flex:1 rather than a hard-coded height. --><div style="display: flex; flex-direction: column; height: 100dvh"> <kai-chat [messages]="messages" (kai-submit)="onSubmit($event)" style="flex: 1; min-height: 0" ></kai-chat></div>Compose individual elements
Section titled “Compose individual elements”<kai-chat> is one option. You can assemble your own layout from individual elements. This example pairs a <kai-conversations> sidebar with a <kai-chat> thread:
import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
@Component({ selector: 'app-workspace', standalone: true, templateUrl: './workspace.component.html', schemas: [CUSTOM_ELEMENTS_SCHEMA],})export class WorkspaceComponent { conversations = myConversations; activeId = this.conversations[0]?.id; messages = loadMessages(this.activeId);
onConversationSelect(e: Event) { this.activeId = (e as CustomEvent<{ id: string }>).detail.id; this.messages = loadMessages(this.activeId); }
onSubmit(e: Event) { const { value } = (e as CustomEvent<{ value: string }>).detail; sendMessage(value); }}<div style="display: flex; height: 100dvh"> <kai-conversations [conversations]="conversations" [activeId]="activeId" (kai-conversation-select)="onConversationSelect($event)" (kai-new-chat)="startNewConversation()" style="width: 300px; flex-shrink: 0" ></kai-conversations>
<kai-chat [messages]="messages" (kai-submit)="onSubmit($event)" style="flex: 1; min-width: 0" ></kai-chat></div>Resizable panels
Section titled “Resizable panels”Wrap panels in <kai-resizable> with one <kai-resizable-item> each to add a draggable divider. Each item takes a size (px or %) and optional min/max. Listen for (kai-change) to persist the layout.
<div style="display: flex; flex-direction: column; height: 100dvh"> <kai-resizable orientation="horizontal" (kai-change)="onResize($event)" style="flex: 1; min-height: 0" > <kai-resizable-item size="25%" min="200px"> <kai-conversations [conversations]="conversations" [activeId]="activeId" (kai-conversation-select)="onConversationSelect($event)" ></kai-conversations> </kai-resizable-item> <kai-resizable-item> <kai-chat [messages]="messages" (kai-submit)="onSubmit($event)" ></kai-chat> </kai-resizable-item> </kai-resizable></div>onResize(e: Event) { const { sizes } = (e as CustomEvent<{ sizes: number[] }>).detail; // persist to localStorage, a service, etc. localStorage.setItem('panel-sizes', JSON.stringify(sizes));}Props and events
Section titled “Props and events”Rich data in as properties; interactions out as events. Angular’s [prop]="…" binding writes to the element’s DOM property directly, so arrays and objects pass through unstringified — no .prop modifier needed.
(kai-event)="handler($event)" listens for a DOM CustomEvent. The payload is on $event.detail:
| Element | Property binding | Event | $event.detail |
|---|---|---|---|
kai-chat | [messages]="messages" | (kai-submit) | { value: string } |
kai-conversations | [conversations]="conversations" | (kai-conversation-select) | { id: string } |
kai-conversations | [groups]="groups" | (kai-new-chat) | — |
kai-conversations | — | (kai-toggle-sidebar) | — |
kai-resizable | orientation="horizontal" | (kai-change) | { sizes: number[] } |
kai-resizable-item | size="25%" min="200px" | — | — |
Standalone display elements
Section titled “Standalone display elements”You can drop individual display elements anywhere in your UI without adopting a full chat shell. <kai-markdown>, <kai-code-block>, <kai-artifact>, <kai-reasoning>, and <kai-tool> all render rich AI content as standalone elements:
<kai-markdown [content]="assistantReply"></kai-markdown><kai-reasoning [text]="thinkingText" (kai-open-change)="onReasoningToggle($event)"></kai-reasoning><kai-artifact [files]="files" (kai-file-select)="onFileSelect($event)"></kai-artifact>Each fills its container and is controlled entirely through [prop] bindings and (event) listeners — no Shadow DOM piercing, no CSS manipulation.
Next steps
Section titled “Next steps”- Installation — package exports and theme tokens
- Components — full prop and event reference for every
kai-*element