Rendering

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

DependencyVersion
Angular>=17.0.0
TypeScript>=5.5.0
@comark/angular is bundler-agnostic — it works with the Angular CLI, Vite (via @analogjs/vite-plugin-angular), Webpack, esbuild, or any other build tool that supports Angular.

Installation

pnpm add @comark/angular

<comark>

The <comark> component is the simplest way to render markdown in Angular. It handles parsing and rendering automatically.

app.component.ts
import { Component } from '@angular/core'
import { ComarkComponent } from '@comark/angular'

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

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

Props (Inputs)

InputTypeDefaultDescription
markdownstring''Markdown content to parse and render
optionsParseOptions{}Parser options (autoUnwrap, autoClose, etc.)
pluginsComarkPlugin[][]Array of plugins
componentsRecord<string, Type<any>>{}Custom Angular component mappings
streamingbooleanfalseEnable streaming mode
summarybooleanfalseOnly render content before <!-- more -->
caretboolean | { class: string }falseAppend caret to last text node
dataRecord<string, unknown>{}Runtime values referenced from markdown via :prop="data.path"

options

See ParseOptions for available options.

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

plugins

See ComarkPlugin for available plugins.

app.component.ts
import { Component } from '@angular/core'
import { ComarkComponent } from '@comark/angular'
import highlight from '@comark/angular/plugins/highlight'
import githubLight from '@shikijs/themes/github-light'
import githubDark from '@shikijs/themes/github-dark'

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

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

app.component.ts
import { Component } from '@angular/core'
import { ComarkComponent } 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: [ComarkComponent],
  template: `
    <comark
      [markdown]="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.

Create a component

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

app.component.ts
import { Component } from '@angular/core'
import { ComarkComponent } from '@comark/angular'
import { AlertComponent } from './components/alert.component'
import { CardComponent } from './components/card.component'

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

Use it in your Markdown

::alert{type="warning"}
This is a warning message!
::
See Component Bindings for how props map to your Angular component inputs, or Component Syntax for the full Comark syntax API — nested components, inline syntax, and more.

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 for the full scope.

app.component.ts
import { Component } from '@angular/core'
import { ComarkComponent } from '@comark/angular'

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

<comark-renderer>

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

Parsing

Parse your markdown content and pass the tree directly:

docs.component.ts
import { Component, OnInit } from '@angular/core'
import { ComarkRendererComponent } from '@comark/angular'
import { parse } from 'comark'
import type { ComarkTree } from 'comark'

@Component({
  selector: 'app-docs',
  standalone: true,
  imports: [ComarkRendererComponent],
  template: `
    @if (tree) {
      <comark-renderer [tree]="tree" [components]="components" />
    }
  `,
})
export class DocsComponent implements OnInit {
  tree: ComarkTree | null = null
  components = {}

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

Renderer Props (Inputs)

InputTypeDefaultDescription
treeComarkTreeRequired. The parsed tree returned by parse()
componentsRecord<string, Type<any>>{}Custom Angular component mappings
streamingbooleanfalseEnable streaming mode
caretboolean | { class: string }falseAppend a blinking caret to the last text node
dataRecord<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:

MarkdownAngular 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 />):

@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 = ''
}

Overriding HTML Elements

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

Create an override component

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

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

Resolution Order

Components are resolved in this order:

  1. Prose{PascalTag} — e.g., ProseH1 for h1
  2. {tag} — e.g., alert
  3. {PascalTag} — e.g., Alert for 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:

components/ai-chat.component.ts
import { Component } from '@angular/core'
import { ComarkComponent } from '@comark/angular'

@Component({
  selector: 'app-ai-chat',
  standalone: true,
  imports: [ComarkComponent],
  template: `
    <comark
      [markdown]="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
  }
}
autoClose is enabled by default — incomplete syntax like **bold text is automatically closed on every parse. Disable with [options]="{ autoClose: false }".

Caret

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

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

<!-- Custom caret class -->
<comark [markdown]="content" [streaming]="isStreaming" [caret]="{ class: 'my-caret' }" />
.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 ComarkElement to type the __node input in components that override HTML elements:

comark-wrapper.component.ts
import { Component, Input, Type } from '@angular/core'
import { ComarkComponent } from '@comark/angular'
import type { ComarkPlugin } from 'comark'

@Component({
  selector: 'app-comark-wrapper',
  standalone: true,
  imports: [ComarkComponent],
  template: `<comark [markdown]="content" [components]="components" [plugins]="plugins" />`,
})
export class ComarkWrapperComponent {
  @Input() content = ''
  @Input() components: Record<string, Type<any>> = {}
  @Input() plugins: ComarkPlugin[] = []
}
components/heading.component.ts
import { Component, Input } from '@angular/core'
import type { ComarkElement } from 'comark'

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

Pre-configured Components

Use defineComarkComponent and defineComarkRendererComponent to create pre-configured wrappers with default plugins, components, and styling baked in — no need to repeat the same config on every instance.

defineComarkComponent

components/docs-comark.ts
import { defineComarkComponent } from '@comark/angular'
import highlight from '@comark/angular/plugins/highlight'
import math, { Math } from '@comark/angular/plugins/math'
import mermaid, { Mermaid } from '@comark/angular/plugins/mermaid'

export const DocsComark = defineComarkComponent({
  name: 'docs-comark',
  plugins: [highlight(), math(), mermaid()],
  components: { Math, Mermaid },
  class: 'prose dark:prose-invert',
})

Then use it like any other component:

page.component.ts
import { Component } from '@angular/core'
import { DocsComark } from './components/docs-comark'

@Component({
  selector: 'app-page',
  standalone: true,
  imports: [DocsComark],
  template: `<docs-comark [markdown]="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.

defineComarkRendererComponent

Same idea, but for the low-level renderer:

components/docs-renderer.ts
import { defineComarkRendererComponent } from '@comark/angular'
import { Math } from '@comark/angular/plugins/math'

export const DocsRenderer = defineComarkRendererComponent({
  name: 'docs-renderer',
  components: { Math },
  class: 'prose',
})