---
title: "Stream Markdown in Angular with the AI SDK"
description: "Build an Angular 17+ chat that streams AI SDK responses and renders each message as Markdown with Comark while it arrives."
canonical_url: "https://comark.dev/use-cases/angular-ai-chat"
---
# Stream Markdown in Angular with the AI SDK

> Build an Angular 17+ chat that streams AI SDK responses and renders each message as Markdown with Comark while it arrives.

Use the `Chat` class from `@ai-sdk/angular` to receive messages, and render each text part with `<comark-markdown [value] [streaming] [caret]>`. Comark closes unfinished syntax while the part streams, and runs a full parse when it is complete.

`@comark/angular` supports Angular 17 or later. See the [requirements](https://comark.dev/rendering/angular#requirements) for the exact range. The `Chat` class from `@ai-sdk/angular` stores its state in Angular signals, so the template updates as tokens arrive.

## Build the chat

::steps{level="3"}
### Install the packages

```bash [Terminal]
npm install @comark/angular ai @ai-sdk/angular
```

### Create the server route

`Chat` sends requests to `/api/chat` by default. This Express route streams the model response in the format that `Chat` expects:

```typescript [server.ts]
import express from 'express'
import { convertToModelMessages, streamText } from 'ai'

const app = express()
app.use(express.json())

app.post('/api/chat', async (req, res) => {
  const result = streamText({
    model: 'anthropic/claude-sonnet-4.6',
    messages: await convertToModelMessages(req.body.messages),
  })

  result.pipeUIMessageStreamToResponse(res)
})

app.listen(3000)
```

Configure the Angular dev server to proxy `/api` to this server. The server API can change between AI SDK versions. Check the [AI SDK docs](https://ai-sdk.dev/docs) for your version.

### Render the messages

Read `chat.messages` in the template. Pass `part.state === 'streaming'` to the `streaming` input:

~~~typescript [chat.component.ts]
import { Component } from '@angular/core'
import { Chat } from '@ai-sdk/angular'
import { Markdown } from '@comark/angular'

@Component({
  selector: 'app-chat',
  standalone: true,
  imports: [Markdown],
  template: `
    @for (message of chat.messages; track message.id) {
      @for (part of message.parts; track $index) {
        @switch (part.type) {
          @case ('text') {
            @if (message.role === 'user') {
              <p>{{ part.text }}</p>
            } @else {
              <comark-markdown [value]="part.text" [streaming]="part.state === 'streaming'" [caret]="true" />
            }
          }
        }
      }
    }

    <form (submit)="send($event, prompt)">
      <input #prompt placeholder="Ask something…" />
    </form>
  `,
})
export class ChatComponent {
  chat = new Chat({})

  send(event: Event, prompt: HTMLInputElement) {
    event.preventDefault()
    this.chat.sendMessage({ text: prompt.value })
    prompt.value = ''
  }
}
~~~
::

## Map components and add plugins

Use the `components` input to map Comark tags to Angular components. Use the `plugins` input to add parser plugins, for example the [security plugin](https://comark.dev/plugins/built-in/security) for untrusted output. This excerpt adds two properties to the component above:

```typescript [chat.component.ts]
import security from '@comark/angular/plugins/security'
import { AlertComponent } from './alert.component'

export class ChatComponent {
  chat = new Chat({})
  components = { alert: AlertComponent }
  plugins = [security({ blockedTags: ['script', 'iframe', 'embed', 'form', 'base', 'meta', 'link', 'style', 'object'] })]
}
```

```html
<comark-markdown
  [value]="part.text"
  [streaming]="part.state === 'streaming'"
  [caret]="true"
  [components]="components"
  [plugins]="plugins"
/>
```

Comark passes component props as `@Input()` values. The default slot maps to `<ng-content />`. See [component bindings](https://comark.dev/rendering/angular#component-bindings).

## FAQ

::accordion
  :::accordion-item{label="Does it work without the Angular CLI?"}
  Yes. `@comark/angular` doesn't depend on a bundler. It works with the Angular CLI, Vite with AnalogJS, webpack, and esbuild.
  :::

  :::accordion-item{label="How do I style the caret?"}
  Pass `[caret]="{ class: 'my-caret' }"` and style that class. See [caret](https://comark.dev/rendering/angular#caret).
  :::

  :::accordion-item{label="How do I render content that is already parsed?"}
  Use `<comark-markdown-document [value]="document" />` with a document from `parseMarkdown()`. It renders the document without a parse step. See [`<comark-markdown-document>`](https://comark.dev/rendering/angular#code-comark-markdown-document).
  :::
::

## Next steps

- [Render streaming Markdown from an LLM](https://comark.dev/use-cases/ai-chat-streaming)
- [Let an LLM render UI components](https://comark.dev/use-cases/generative-ui)
- [Angular streaming reference](https://comark.dev/rendering/angular#streaming)
- [Vite Angular example](https://comark.dev/examples/vite/angular)

---

- [Angular rendering](https://comark.dev/rendering/angular)
- [Vite Angular example](https://comark.dev/examples/vite/angular)


## Sitemap

See the full [sitemap](https://comark.dev/sitemap.md) for all pages.
