Skip to content
kitn AI/UI

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.

Terminal window
npm install @kitn.ai/ui

Import the element bundle once as a side-effect in main.ts, before bootstrapApplication. This registers every kai-* web component globally.

main.ts
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.

app.component.ts
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 {}

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

app.component.ts
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
);
}
}
}
app.component.html
<!-- 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>

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

workspace.component.ts
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);
}
}
workspace.component.html
<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>

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));
}

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:

ElementProperty bindingEvent$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-resizableorientation="horizontal"(kai-change){ sizes: number[] }
kai-resizable-itemsize="25%" min="200px"

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.

  • Installation — package exports and theme tokens
  • Components — full prop and event reference for every kai-* element