# Angular

Use kai-* web components in Angular with CUSTOM_ELEMENTS_SCHEMA, property bindings, and kai-event outputs — no wrappers needed.

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

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

## Register the elements

The elements register **asynchronously**: `@kitn.ai/ui/elements` defers the actual `customElements.define` calls behind a browser check, so an import alone does not guarantee the tags exist yet. Angular writes array and object DOM properties the moment it stamps a `<kai-*>` tag; a write that lands before the element upgrades is discarded when the element applies its own empty defaults. Gate `bootstrapApplication` on the `elementsReady` promise the entry exports:

```ts
// main.ts

// Resolves once every kai-* element is defined. Angular then binds against
// upgraded elements, so [messages] / [conversations] survive.
elementsReady.then(() => 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.

```ts
// app.component.ts

@Component({
  selector: 'app-root',
  standalone: true,
  templateUrl: './app.component.html',
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class AppComponent {}
```

## Theme tokens

Each element styles itself inside Shadow DOM, so nothing is required to make the elements look right. Add the token sheet when you want to override the design tokens, or reuse the kit's `--color-*` values for your own chrome around the chat.

Angular loads global CSS from the build config, **not** from a TS `import './x.css'`, which `@angular/build` ignores. Add the sheet to `architect.build.options.styles`:

```jsonc
// angular.json
"styles": [
  "node_modules/@kitn.ai/ui/dist/theme.tokens.css",
  "src/styles.css"
]
```

`theme.tokens.css` is the plain custom-property sheet. `@kitn.ai/ui/theme.css` is the Tailwind source layer, meant for apps that already compile Tailwind.

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

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.

```ts
// app.component.ts

@Component({
  selector: 'app-root',
  standalone: true,
  templateUrl: './app.component.html',
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class AppComponent {
  messages: ChatMessage[] = [
    { id: '1', role: 'assistant', parts: [{ type: 'text', text: '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: ChatMessage[] = [
      ...this.messages,
      { id: crypto.randomUUID(), role: 'user', parts: [{ type: 'text', text: value }] },
    ];
    this.messages = history;

    const aid = crypto.randomUUID();
    this.messages = [...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.
      this.messages = this.messages.map((m) =>
        m.id === aid ? { ...m, parts: appendTextPart(m.parts, token) } : m
      );
    }
  }
}
```

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

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

```ts
// workspace.component.ts

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

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

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

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

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

**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"` | — | — |

> **note:** 
Scalar attributes like `orientation="horizontal"` or `theme="dark"` can be plain HTML attributes (no brackets). Use `[prop]="…"` only for non-string values — arrays, objects, and Angular expressions.

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

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

## The backend route

`streamFromYourAPI` above is your `POST /api/chat`. **Hosting it inside the Angular app requires SSR.** `@angular/build:dev-server` exposes no middleware hook and takes no Vite plugins, so a browser-only Angular app cannot serve an endpoint at all. Its only options are proxying to a separate server or running the handler elsewhere.

With SSR enabled, the server is a plain Express app:

```bash
ng add @angular/ssr
```

That generates `src/server.ts`. The Angular CLI loads it through the `reqHandler` export at the bottom for **both** `ng serve` and `ng build`, so one registration covers development and production.

```ts
// src/server.ts
const app = express();
const angularApp = new AngularNodeAppEngine();

// ORDER MATTERS: register the endpoint BEFORE the catch-all below. After it,
// Angular renders an HTML page for /api/chat and the browser tries to parse
// that as SSE.
app.post('/api/chat', express.json(), async (req, res) => {
  // call your model, pipe the SSE body through to res
});

app.use(express.static(browserDistFolder, { maxAge: '1y', index: false, redirect: false }));

// Everything else renders the Angular application.
app.use((req, res, next) => {
  angularApp
    .handle(req)
    .then((response) => (response ? writeResponseToNodeResponse(response, res) : next()))
    .catch(next);
});

export const reqHandler = createNodeRequestHandler(app);
```

Two things that bite when piping a provider stream through Express: forward `response.status` (a 401 that reaches the browser as a 200 is a blank bubble and no error), and write each chunk as it lands rather than buffering the body.

The route's contract, the SSE shape, and the parser that folds it back onto `messages` are the same in every framework: see [Connect any backend](/integrations/connect-any-backend/) and the [wire adapter](/guides/recipes/wire-adapter/).

> **tip:** 
The `kai` MCP server scaffolds the Angular front end and this `src/server.ts` together, wired to whichever provider you name. See [For AI Agents](/guides/for-ai-agents/).

## Next steps

- [Installation](/guides/installation/) — package exports and theme tokens
- [Components](/components/attachments/) — full prop and event reference for every `kai-*` element
- [Connect any backend](/integrations/connect-any-backend/) — the contract your `/api/chat` route fills
