---
title: "Render Comark in Angular"
description: "Learn how to render Comark in an Angular 17+ application with standalone components, custom components, plugins, and streaming support."
canonical_url: "https://comark.dev/rendering/angular"
---
# Render Comark in Angular

> Learn how to render Comark in an Angular 17+ application with standalone components, custom components, plugins, and streaming support.

The `@comark/angular` package provides standalone components for rendering Comark content in Angular with full support for custom components, plugins, and streaming.

## Requirements

| Dependency     | Version            |
| -------------- | ------------------ |
| **Angular**    | `>=17.0.0 <22.0.0` |
| **TypeScript** | `>=5.5.0`          |

<callout icon="i-lucide-info" color="info">
`@comark/angular` is **bundler-agnostic**: it works with the Angular CLI, Vite (via [`@analogjs/vite-plugin-angular`](https://analogjs.org)), Webpack, esbuild, or any other build tool that supports Angular.
</callout>

## Installation

<code-group>
```bash [pnpm]
pnpm add @comark/angular
```


```bash [npm]
npm install @comark/angular
```


```bash [yarn]
yarn add @comark/angular
```


```bash [bun]
bun add @comark/angular
```
</code-group>

## `<comark-markdown>`

The `<comark-markdown>` component is the quickest way to render markdown in Angular. It handles parsing and rendering automatically. The `value` input accepts a markdown **string** or a pre-parsed [`MarkdownDocument`](https://comark.dev/getting-started/document-model).

~~~typescript [app.component.ts]
import { Component } from '@angular/core'
import { Markdown } from '@comark/angular'

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [Markdown],
  template: `<comark-markdown [value]="content" />`,
})
export class AppComponent {
  content = `# Hello World

This is **markdown** with Comark components.
`
}
~~~

<callout icon="i-lucide-package" color="warning">
Passing a document to `<comark-markdown>` skips parsing at runtime, but the **parser is still bundled** because `Markdown` imports it. To keep the client bundle free of the parser, use [`<comark-markdown-document>`](#code-comark-markdown-document) instead.
</callout>

### Props (inputs)

| Input                       | Type                            | Default | Description                                                                                                                                      |
| --------------------------- | ------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `value`                     | `string \| MarkdownDocument`    | `''`    | Markdown string or pre-parsed document                                                                                                           |
| [`options`](#code-options)  | `ParserOptions`                 | `{}`    | Parser options (autoUnwrap, autoClose, etc.)                                                                                                     |
| [`plugins`](#code-plugins)  | `ComarkPlugin[]`                | `[]`    | Array of plugins                                                                                                                                 |
| `unwrap`                    | `boolean \| string \| string[]` | `false` | Strip wrapper tags (MDC `unwrap`) — `true` unwraps `<p>`; a comma/space-separated string or array peels tags sequentially, for example `"ul li"` |
| [`components`](#components) | `Record<string, Type<any>>`     | `{}`    | Custom Angular component mappings                                                                                                                |
| [`streaming`](#streaming)   | `boolean`                       | `false` | Enable streaming mode                                                                                                                            |
| `summary`                   | `boolean`                       | `false` | Only render content before `<!-- more -->`                                                                                                       |
| [`caret`](#caret)           | `boolean \| { class: string }`  | `false` | Append caret to last text node                                                                                                                   |
| [`data`](#data)             | `Record<string, unknown>`       | `{}`    | Runtime values referenced from markdown via `:prop="data.path"`                                                                                  |

#### `options`

See [ParserOptions](https://github.com/comarkdown/comark/blob/main/packages/comark/src/types.ts#L247) for available options.

```typescript [app.component.ts]
@Component({
  template: `<comark-markdown [value]="content" [options]="{ autoUnwrap: true, autoClose: true }" />`,
})
```

#### `plugins`

See [ComarkPlugin](https://comark.dev/plugins) for available plugins.

```typescript [app.component.ts]
import { Component } from '@angular/core'
import { Markdown } from '@comark/angular'
import shiki from '@comark/angular/plugins/shiki'
import githubLight from '@shikijs/themes/github-light'
import githubDark from '@shikijs/themes/github-dark'

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [Markdown],
  template: `<comark-markdown [value]="content" [plugins]="plugins" />`,
})
export class AppComponent {
  content = '```js\nconsole.log("hello")\n```'
  plugins = [
    shiki({
      themes: { light: githubLight, dark: githubDark },
    }),
  ]
}
```

For math and mermaid plugins, also pass the companion components:

~~~typescript [app.component.ts]
import { Component } from '@angular/core'
import { Markdown } from '@comark/angular'
import math, { Math } from '@comark/angular/plugins/math'
import mermaid, { Mermaid } from '@comark/angular/plugins/mermaid'

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [Markdown],
  template: `
    <comark-markdown
      [value]="content"
      [plugins]="plugins"
      [components]="components"
    />
  `,
})
export class AppComponent {
  content = '$E = mc^2$'
  plugins = [math(), mermaid()]
  components = { Math, Mermaid }
}
~~~

#### `components`

Use this input to map custom Angular components to Comark elements and use them in your markdown.

<steps level="4">
#### Create a component


~~~typescript [components/alert.component.ts]
import { Component, Input } from '@angular/core'

@Component({
  selector: 'app-alert',
  standalone: true,
  template: `
    <div class="alert" [class]="'alert-' + type" role="alert">
      <ng-content />
    </div>
  `,
})
export class AlertComponent {
  @Input() type: 'info' | 'warning' | 'error' | 'success' = 'info'
}
~~~


#### Map the tag to your component


```typescript [app.component.ts]
import { Component } from '@angular/core'
import { Markdown } from '@comark/angular'
import { AlertComponent } from './components/alert.component'
import { CardComponent } from './components/card.component'

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [Markdown],
  template: `<comark-markdown [value]="content" [components]="components" />`,
})
export class AppComponent {
  content = '...'
  components = { alert: AlertComponent, card: CardComponent }
}
```


#### Use it in your Markdown


```mdc
::alert{type="warning"}
This is a warning message!
::
```
</steps>

<tip>
See [Component Bindings](#component-bindings) for how props map to your Angular component inputs, or [Component Syntax](https://comark.dev/syntax/components) for the full Comark syntax API: nested components, inline syntax, and more.
</tip>

#### `data`

Expose runtime values to markdown authors. Any prop written with a `:` prefix is resolved against the render context `{ frontmatter, meta, data, props }` when its value isn't valid JSON. See [Data Binding](https://comark.dev/syntax/components#data-binding) for the full scope.

```typescript [app.component.ts]
import { Component } from '@angular/core'
import { Markdown } from '@comark/angular'

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [Markdown],
  template: `<comark-markdown [value]="content" [data]="data" />`,
})
export class AppComponent {
  data = { user: { name: 'Ada', role: 'admin' } }
  content = `Hello, :badge{:label="data.user.name"}!`
}
```

---

## `<comark-markdown-document>`

Renders a pre-parsed `MarkdownDocument` without any parsing. Use it when you parse on the server, in a build step, or via an API, so no parser or plugin code is shipped to the browser.

### Parsing

Parse your markdown content and pass the document directly:

~~~typescript [docs.component.ts]
import { Component, OnInit } from '@angular/core'
import { MarkdownDocument } from '@comark/angular'
import { parseMarkdown } from 'comark'
import type { MarkdownDocument as MarkdownDocumentType } from 'comark'

@Component({
  selector: 'app-docs',
  standalone: true,
  imports: [MarkdownDocument],
  template: `
    @if (document) {
      <comark-markdown-document [value]="document" [components]="components" />
    }
  `,
})
export class DocsComponent implements OnInit {
  document: MarkdownDocumentType | null = null
  components = {}

  async ngOnInit() {
    const markdown = await fetch('/api/content').then((r) => r.text())
    this.document = await parseMarkdown(markdown)
  }
}
~~~

### Renderer props (inputs)

| Input                            | Type                           | Default | Description                                                     |
| -------------------------------- | ------------------------------ | ------- | --------------------------------------------------------------- |
| `value`                          | `MarkdownDocument`             | —       | **Required.** The parsed document returned by `parseMarkdown()` |
| [`components`](#code-components) | `Record<string, Type<any>>`    | `{}`    | Custom Angular component mappings                               |
| [`streaming`](#streaming)        | `boolean`                      | `false` | Enable streaming mode                                           |
| [`caret`](#caret)                | `boolean \| { class: string }` | `false` | Append a blinking caret to the last text node                   |
| [`data`](#data)                  | `Record<string, unknown>`      | `{}`    | Runtime values referenced from markdown via `:prop="data.path"` |

---

## Component bindings

Comark automatically bridges the gap between Comark syntax and your Angular component's interface.

### Prop binding

Attributes in Comark syntax are passed as `@Input()` values to your component. Use the `:` prefix to pass typed values:

| Markdown                    | Angular Input value       |
| --------------------------- | ------------------------- |
| `{type="warning"}`          | `"warning"` (string)      |
| `{:count="5"}`              | `5` (number)              |
| `{:active="true"}`          | `true` (boolean)          |
| `{:config='{"key":"val"}'}` | `{ key: 'val' }` (object) |

### Content projection

Default content in Comark maps to Angular's content projection (`<ng-content />`):

<code-group>
~~~typescript [Angular]
@Component({
  selector: 'app-card',
  standalone: true,
  template: `
    <div class="card">
      <h3>{{ title }}</h3>
      <div class="card-body">
        <ng-content />
      </div>
    </div>
  `,
})
export class CardComponent {
  @Input() title = ''
}
~~~


```mdc [Comark]
::card{title="My Card"}
Default content goes here.
::
```
</code-group>

---

## Overriding HTML elements

Use the `Prose` prefix to override how native HTML elements render:

<steps level="3">
### Create an override component


~~~typescript [components/prose-h1.component.ts]
import { Component, Input } from '@angular/core'

@Component({
  selector: 'prose-h1',
  standalone: true,
  template: `
    <h1 [id]="id" class="custom-heading">
      <ng-content />
    </h1>
  `,
})
export class ProseH1Component {
  @Input() id?: string
}
~~~


### Map it via the `components` input


```typescript [app.component.ts]
@Component({
  template: `<comark-markdown [value]="content" [components]="components" />`,
})
export class AppComponent {
  components = { ProseH1: ProseH1Component }
}
```
</steps>

### Resolution order

Components are resolved in this order:

1. `Prose{PascalTag}`: for example, `ProseH1` for `h1`
2. `{PascalTag}`: for example, `Alert` for `alert`
3. `{tag}`: for example, `alert`

If no custom component matches, the tag renders as a native HTML element.

---

## Streaming

Enable real-time rendering as content arrives, ideal for AI chat interfaces and live previews.

Set `streaming` to `true` while content is being received, then `false` when done:

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

@Component({
  selector: 'app-ai-chat',
  standalone: true,
  imports: [Markdown],
  template: `
    <comark-markdown
      [value]="content"
      [streaming]="isStreaming"
      [caret]="true"
    />
  `,
})
export class AiChatComponent {
  content = ''
  isStreaming = false

  async askAI(prompt: string) {
    this.content = ''
    this.isStreaming = true

    const response = await fetch('/api/chat', {
      method: 'POST',
      body: JSON.stringify({ prompt }),
    })

    const reader = response.body!.getReader()
    const decoder = new TextDecoder()

    while (true) {
      const { done, value } = await reader.read()
      if (done) break
      this.content += decoder.decode(value, { stream: true })
    }

    this.isStreaming = false
  }
}
~~~

<callout icon="i-lucide-info" color="info">
`autoClose` is enabled by default: incomplete syntax like `**bold text` is automatically closed on every parse. Disable with `[options]="{ autoClose: false }"`.
</callout>

### Caret

The `caret` input appends a blinking cursor to the last text node while `streaming` is `true`:

```html
<!-- Default caret -->
<comark-markdown [value]="content" [streaming]="isStreaming" [caret]="true" />

<!-- Custom caret class -->
<comark-markdown [value]="content" [streaming]="isStreaming" [caret]="{ class: 'my-caret' }" />
```

```css
.my-caret {
  display: inline-block;
  width: 2px;
  height: 1em;
  background: currentColor;
  animation: blink 1s step-end infinite;
  vertical-align: text-bottom;
}

@keyframes blink {
  50% { opacity: 0; }
}
```

---

## TypeScript support

Use `ComarkPlugin` from `comark` to type plugin arrays, and `ElementNode` to type the `__node` input in components that override HTML elements:

```typescript [comark-wrapper.component.ts]
import { Component, Input, Type } from '@angular/core'
import { Markdown } from '@comark/angular'
import type { ComarkPlugin } from 'comark'

@Component({
  selector: 'app-comark-wrapper',
  standalone: true,
  imports: [Markdown],
  template: `<comark-markdown [value]="content" [components]="components" [plugins]="plugins" />`,
})
export class ComarkWrapperComponent {
  @Input() content = ''
  @Input() components: Record<string, Type<any>> = {}
  @Input() plugins: ComarkPlugin[] = []
}
```

~~~typescript [components/heading.component.ts]
import { Component, Input } from '@angular/core'
import type { ElementNode } from 'comark'

@Component({
  selector: 'app-heading',
  standalone: true,
  template: `
    <h2 [id]="id" class="heading">
      <ng-content />
    </h2>
  `,
})
export class HeadingComponent {
  @Input() __node?: ElementNode
  @Input() id?: string
}
~~~

---

## Pre-configured components

Use `defineMarkdownComponent` and `defineMarkdownDocumentComponent` to create pre-configured wrappers with default plugins, components, and styling baked in, so there is no need to repeat the same config on every instance.

### `defineMarkdownComponent`

```typescript [components/docs-markdown.ts]
import { defineMarkdownComponent } from '@comark/angular'
import shiki from '@comark/angular/plugins/shiki'
import math, { Math } from '@comark/angular/plugins/math'
import mermaid, { Mermaid } from '@comark/angular/plugins/mermaid'

export const DocsMarkdown = defineMarkdownComponent({
  plugins: [shiki(), math(), mermaid()],
  components: { Math, Mermaid },
  class: 'prose dark:prose-invert',
})
```

Then use it like any other component. The returned component always uses the `comark-markdown-defined` selector:

```typescript [page.component.ts]
import { Component } from '@angular/core'
import { DocsMarkdown } from './components/docs-markdown'

@Component({
  selector: 'app-page',
  standalone: true,
  imports: [DocsMarkdown],
  template: `<comark-markdown-defined [value]="content" />`,
})
export class PageComponent {
  content = '# Hello\n\n$E = mc^2$'
}
```

Instance-level `[plugins]` and `[components]` inputs are merged with (and override) the config-level defaults.

### `defineMarkdownDocumentComponent`

Same idea, but for the low-level renderer. The returned component always uses the `comark-markdown-document-defined` selector:

```typescript [components/docs-markdown-document.ts]
import { defineMarkdownDocumentComponent } from '@comark/angular'
import { Math } from '@comark/angular/plugins/math'

export const DocsMarkdownDocument = defineMarkdownDocumentComponent({
  components: { Math },
  class: 'prose',
})
```

---

---

- [Plugins](https://comark.dev/plugins)
- [Streaming API](https://comark.dev/reference/auto-close)


## Sitemap

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