Render Comark in Angular
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 |
| 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/angularnpm install @comark/angularyarn add @comark/angularbun add @comark/angular<comark>
The <comark> component is the simplest way to render markdown in Angular. It handles parsing and rendering automatically.
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)
| Input | Type | Default | Description |
|---|---|---|---|
markdown | string | '' | Markdown content to parse and render |
options | ParseOptions | {} | Parser options (autoUnwrap, autoClose, etc.) |
plugins | ComarkPlugin[] | [] | Array of plugins |
components | Record<string, Type<any>> | {} | Custom Angular component mappings |
streaming | boolean | false | Enable streaming mode |
summary | boolean | false | Only render content before <!-- more --> |
caret | boolean | { class: string } | false | Append caret to last text node |
data | Record<string, unknown> | {} | Runtime values referenced from markdown via :prop="data.path" |
options
See ParseOptions for available options.
@Component({
template: `<comark [markdown]="content" [options]="{ autoUnwrap: true, autoClose: true }" />`,
})plugins
See ComarkPlugin for available plugins.
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:
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
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
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!
::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.
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:
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)
| Input | Type | Default | Description |
|---|---|---|---|
tree | ComarkTree | — | Required. The parsed tree returned by parse() |
components | Record<string, Type<any>> | {} | Custom Angular component mappings |
streaming | boolean | false | Enable streaming mode |
caret | boolean | { class: string } | false | Append a blinking caret to the last text node |
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 />):
@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 = ''
}::card{title="My Card"}
Default content goes here.
::Overriding HTML Elements
Use the Prose prefix to override how native HTML elements render:
Create an override component
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
@Component({
template: `<comark [markdown]="content" [components]="components" />`,
})
export class AppComponent {
components = { ProseH1: ProseH1Component }
}Resolution Order
Components are resolved in this order:
Prose{PascalTag}— e.g.,ProseH1forh1{tag}— e.g.,alert{PascalTag}— e.g.,Alertforalert
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:
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:
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[] = []
}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
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:
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:
import { defineComarkRendererComponent } from '@comark/angular'
import { Math } from '@comark/angular/plugins/math'
export const DocsRenderer = defineComarkRendererComponent({
name: 'docs-renderer',
components: { Math },
class: 'prose',
})