---
title: "Render Comark in Vue"
description: "Learn how to render Comark in a Vue.js application with custom components, plugins, and streaming support."
canonical_url: "https://comark.dev/rendering/vue"
---
# Render Comark in Vue

> Learn how to render Comark in a Vue.js application with custom components, plugins, and streaming support.

The `@comark/vue` package provides Vue components for rendering Comark content with full support for custom components, plugins, and streaming.

## Installation

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


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


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


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

## `<Markdown>`

The `<Markdown>` component is the quickest way to render markdown in Vue. It handles parsing and rendering automatically.

<warning>
`<Markdown>` is an **async** component and must always be wrapped in `<Suspense>`
</warning>

For server-side rendering, use the [`MarkdownDocument`](#code-markdowndocument) component combined with parsing on the server instead.

~~~vue [App.vue]
<script setup lang="ts">
import { Markdown } from '@comark/vue'

const content = `# Hello World

This is **markdown** with Comark components.
`
</script>

<template>
  <Suspense>
    <Markdown>{{ content }}</Markdown>
  </Suspense>
</template>
~~~

### Usage

Pass markdown content via the default slot or the `value` prop. `value` accepts a markdown **string** or a pre-parsed [`MarkdownDocument`](https://comark.dev/getting-started/document-model):

<code-group>
```vue [Default Slot]
<template>
  <Markdown>{{ content }}</Markdown>
</template>
```


```vue [String]
<template>
  <Markdown :value="content" />
</template>
```


```vue [MarkdownDocument]
<script setup lang="ts">
import type { MarkdownDocument } from 'comark'

defineProps<{ document: MarkdownDocument }>()
</script>

<template>
  <Markdown :value="document" />
</template>
```
</code-group>

<callout icon="i-lucide-package" color="warning">
Passing a document to `<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 [`<MarkdownDocument>`](#code-markdowndocument) instead.
</callout>

### Props

| Prop                                                                 | Type                            | Default     | Description                                                                                                                                      |
| -------------------------------------------------------------------- | ------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `value`                                                              | `string \| MarkdownDocument`    | `undefined` | Markdown string or pre-parsed document (alternative to default slot)                                                                             |
| [`options`](#code-markdown-props-code-options)                       | `ParserOptions`                 | `{}`        | Parser options (autoUnwrap, autoClose, etc.)                                                                                                     |
| [`plugins`](#code-markdown-props-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`](#code-markdown-props-code-components)                 | `Record<string, Component>`     | `{}`        | Custom Vue component mappings                                                                                                                    |
| [`componentsManifest`](#code-markdown-props-code-componentsmanifest) | `ComponentManifest`             | `undefined` | Dynamic component resolver                                                                                                                       |
| [`streaming`](#streaming)                                            | `boolean`                       | `false`     | Enable streaming mode                                                                                                                            |
| `summary`                                                            | `boolean`                       | `false`     | Only render content before `<!-- more -->`                                                                                                       |
| [`caret`](#streaming-caret)                                          | `boolean \| { class: string }`  | `false`     | Append caret to last text node                                                                                                                   |
| [`data`](#code-markdown-props-code-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.

```vue [App.vue]
<script setup lang="ts">
import { Markdown } from '@comark/vue'
</script>

<template>
  <Markdown :options="{ autoUnwrap: true, autoClose: true }">
    {{ content }}
  </Markdown>
</template>
```

#### `plugins`

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

```vue [App.vue]
<script setup lang="ts">
import { Markdown } from '@comark/vue'
import shiki from '@comark/vue/plugins/shiki'
import githubLight from '@shikijs/themes/github-light'
import githubDark from '@shikijs/themes/github-dark'

const plugins = [
  shiki({
    themes: {
      light: githubLight,
      dark: githubDark
    }
  })
]
</script>

<template>
  <Suspense>
    <Markdown :plugins="plugins">{{ content }}</Markdown>
  </Suspense>
</template>
```

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

```vue [App.vue]
<script setup lang="ts">
import { Markdown } from '@comark/vue'
import math, { Math } from '@comark/vue/plugins/math'
import mermaid, { Mermaid } from '@comark/vue/plugins/mermaid'
import 'katex/dist/katex.min.css'
</script>

<template>
  <Suspense>
    <Markdown
      :value="markdown"
      :components="{ math: Math, mermaid: Mermaid }"
      :plugins="[math(), mermaid()]"
    />
  </Suspense>
</template>
```

#### `components`

Use this prop to map custom Vue components to Comark elements and use them in your markdown.

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


Save a component such as `components/Alert.vue`:


```vue [components/Alert.vue]
<script setup lang="ts">
defineProps<{
  type?: 'info' | 'warning' | 'error' | 'success'
}>()
</script>

<template>
  <div class="alert" :class="`alert-${type || 'info'}`" role="alert">
    <slot />
  </div>
</template>
```


#### Map the tag to your component


Pass the `components` prop to `Markdown`:


```vue [App.vue]
<script setup lang="ts">
import { Markdown } from '@comark/vue'
import Alert from './components/Alert.vue'
import Card from './components/Card.vue'

const components = { alert: Alert, card: Card }
</script>

<template>
  <Markdown :components="components">{{ content }}</Markdown>
</template>
```


<note>
Components receive props from the markdown and render children via slots.
</note>
#### Use it in your Markdown content


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

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

#### `componentsManifest`

For lazy-loading components on demand. Components are resolved once and cached. Works with both `<Markdown>` and `<MarkdownDocument>`:

```vue [App.vue]
<script setup lang="ts">
import { Markdown } from '@comark/vue'

const manifest = (name: string) => {
  return import(`./components/prose/${name}.vue`)
}
</script>

<template>
  <Markdown :components-manifest="manifest">{{ content }}</Markdown>
</template>
```

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

```vue [App.vue]
<script setup lang="ts">
import { Markdown } from '@comark/vue'

const user = { name: 'Ada', role: 'admin' }
const content = `Hello, :badge{:label="data.user.name"}!`
</script>

<template>
  <Suspense>
    <Markdown :value="content" :data="{ user }" />
  </Suspense>
</template>
```

### `defineMarkdownComponent`

Creates a pre-configured `<Markdown>` component with default options, plugins, and components baked in.

#### Usage

<steps level="4">
#### Expose your configured component


```typescript [markdown.ts]
import { defineMarkdownComponent } from '@comark/vue'
import shiki from '@comark/vue/plugins/shiki'
import math, { Math } from '@comark/vue/plugins/math'
import mermaid, { Mermaid } from '@comark/vue/plugins/mermaid'
import githubLight from '@shikijs/themes/github-light'
import githubDark from '@shikijs/themes/github-dark'
import CustomAlert from './components/CustomAlert.vue'

export const AppMarkdown = defineMarkdownComponent({
  name: 'AppMarkdown',

  plugins: [
    math(),
    mermaid(),
    shiki({
      themes: {
        light: githubLight,
        dark: githubDark
      },
    }),
  ],

  components: {
    Math,
    Mermaid,
    alert: CustomAlert,
  },
})
```


#### Use it in your templates


```vue [App.vue]
<script setup lang="ts">
import { AppMarkdown } from './markdown'
</script>

<template>
  <!-- All configuration is already included -->
  <AppMarkdown>{{ content }}</AppMarkdown>

  <!-- Can still override per-instance -->
  <AppMarkdown :components="{ alert: DifferentAlert }">{{ content }}</AppMarkdown>
</template>
```
</steps>

#### Options

| Option                                                                | Type                                         | Default     | Description                                                                                        |
| --------------------------------------------------------------------- | -------------------------------------------- | ----------- | -------------------------------------------------------------------------------------------------- |
| [`extends`](#code-markdown-code-definemarkdowncomponent-code-extends) | `ReturnType<typeof defineMarkdownComponent>` | `undefined` | Inherit plugins and components from another component                                              |
| `name`                                                                | `string`                                     | `undefined` | Component name for debugging                                                                       |
| `autoUnwrap`                                                          | `boolean`                                    | `true`      | Automatically unwrap single block elements                                                         |
| `autoClose`                                                           | `boolean`                                    | `true`      | Auto-close incomplete markdown syntax                                                              |
| `linkify`                                                             | `boolean`                                    | `true`      | Auto-convert URL-like text into links                                                              |
| `registerDefaultPlugins`                                              | `boolean`                                    | `true`      | Register default plugins (`frontmatter`, `html`, `alert`, `task-list`, `components`, `attributes`) |
| [`plugins`](#code-markdown-props-code-plugins)                        | `ComarkPlugin[]`                             | `[]`        | Array of plugins                                                                                   |
| [`components`](#code-markdown-props-code-components)                  | `Record<string, Component>`                  | `{}`        | Custom Vue component mappings                                                                      |
| `class`                                                               | `string`                                     | `undefined` | Additional CSS classes for the wrapper div                                                         |

#### `extends`

Inherit plugins and components from another component, then layer your own on top:

```typescript [markdown.ts]
import { defineMarkdownComponent } from '@comark/vue'
import shiki from '@comark/vue/plugins/shiki'
import githubLight from '@shikijs/themes/github-light'
import githubDark from '@shikijs/themes/github-dark'
import toc from '@comark/vue/plugins/toc'
import math, { Math } from '@comark/vue/plugins/math'
import ProsePre from './components/ProsePre.vue'

// Base: highlight + prose components, used everywhere
export const BaseMarkdown = defineMarkdownComponent({
  name: 'BaseMarkdown',
  plugins: [
    shiki({ themes: { light: githubLight, dark: githubDark } }),
  ],
  components: { ProsePre },
})

// Article: extends Base, adds TOC and math
export const ArticleMarkdown = defineMarkdownComponent({
  name: 'ArticleMarkdown',
  extends: BaseMarkdown,
  plugins: [toc({ depth: 3 }), math()],
  components: { Math },
})

// Comment: extends Base only, no TOC, no math
export const CommentMarkdown = defineMarkdownComponent({
  name: 'CommentMarkdown',
  extends: BaseMarkdown,
})
```

#### Merging behavior

- `plugins`: Arrays are concatenated (config plugins + prop plugins)
- `components`: Component mappings override global configuration
- Other `options`: Component options override global configuration

---

## `<MarkdownDocument>`

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

<steps level="4">
#### Parse on the server


```typescript [server.ts]
import { createMarkdownParser } from 'comark'
import { readFile } from 'node:fs/promises'

const parse = createMarkdownParser()

// In your server handler
export async function getContentDocument(slug: string) {
  const markdown = await readFile(`content/${slug}.md`, 'utf-8')
  return parse(markdown)
}
```


#### Render with `MarkdownDocument`


```vue [ContentPage.vue]
<script setup lang="ts">
import { MarkdownDocument } from '@comark/vue'
import Alert from './components/Alert.vue'

const { slug } = defineProps<{ slug: string }>()

const res = await fetch(`/api/content/${slug}`)
const document = await res.json()
</script>

<template>
  <MarkdownDocument :value="document" :components="{ alert: Alert }" />
</template>
```
</steps>

### Renderer props

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

### `defineMarkdownDocumentComponent`

Creates a pre-configured `<MarkdownDocument>` with baked-in component mappings.

#### Setup

<steps level="4">
#### Expose your configured renderer


```typescript [markdown.ts]
import { defineMarkdownDocumentComponent } from '@comark/vue'
import CustomAlert from './components/Alert.vue'
import ProsePre from './components/ProsePre.vue'

export const ArticleMarkdownDocument = defineMarkdownDocumentComponent({
  name: 'ArticleMarkdownDocument',
  components: {
    alert: CustomAlert,
    ProsePre,
  },
})
```


#### Use it in your templates


```vue [ArticlePage.vue]
<script setup lang="ts">
import { ArticleMarkdownDocument } from './markdown'

const { slug } = defineProps<{ slug: string }>()

const res = await fetch(`/api/article/${slug}`)
const document = await res.json()
</script>

<template>
  <ArticleMarkdownDocument :value="document" />
</template>
```
</steps>

#### Renderer options

| Option                                                                               | Type                                                 | Default     | Description                                      |
| ------------------------------------------------------------------------------------ | ---------------------------------------------------- | ----------- | ------------------------------------------------ |
| [`extends`](#code-markdowndocument-code-definemarkdowndocumentcomponent-inheritance) | `ReturnType<typeof defineMarkdownDocumentComponent>` | `undefined` | Inherit component mappings from another renderer |
| `name`                                                                               | `string`                                             | `undefined` | Component name for debugging                     |
| [`components`](#code-markdown-props-code-components)                                 | `Record<string, Component>`                          | `{}`        | Custom Vue component mappings                    |
| `class`                                                                              | `string`                                             | `undefined` | Additional CSS classes for the wrapper div       |

#### Inheritance

Inherit component mappings from another renderer, then layer your own on top:

```typescript [markdown.ts]
import { defineMarkdownDocumentComponent } from '@comark/vue'
import ProsePre from './components/ProsePre.vue'
import ProseA from './components/ProseA.vue'
import CustomAlert from './components/Alert.vue'
import CommentAlert from './components/CommentAlert.vue'

const BaseMarkdownDocument = defineMarkdownDocumentComponent({
  name: 'BaseMarkdownDocument',
  components: { ProsePre, ProseA },
})

export const ArticleMarkdownDocument = defineMarkdownDocumentComponent({
  name: 'ArticleMarkdownDocument',
  extends: BaseMarkdownDocument,
  components: { alert: CustomAlert },
})

export const CommentMarkdownDocument = defineMarkdownDocumentComponent({
  name: 'CommentMarkdownDocument',
  extends: BaseMarkdownDocument,
  components: { alert: CommentAlert },
})
```

---

## Live documents

`MarkdownDocument` can subscribe to an ambient **context** so external sources can drive a mounted renderer: an HMR signal, a collaboration socket, an agent editing the document while you chat with it, or devtools. The listen key is the document's own `meta.key` (set by a plugin) or the `document-key` prop, so a parsed document can carry its own identity. If `globalThis.comarkContext` exists the renderer listens for updates on that key and re-renders; on unmount it cleans up. With no context, the key is ignored at zero cost.

```vue
<template>
  <MarkdownDocument document-key="page" :value="document" />
</template>
```

A **driver** installs the context once and pushes updates by key with `set()` (replace the whole document) or `patch()` (surgical node edits, with structural sharing so only the changed branch re-renders):

```ts
import { createComarkContext, parseMarkdown } from 'comark'

const ctx = createComarkContext() // installs globalThis.comarkContext
const doc = ctx.get('page', await parseMarkdown('# Hello')) // seed on first access

doc.set(await parseMarkdown('# Replaced'))
doc.patch({ op: 'insert', path: [1], node: ['p', {}, 'inserted'] })
```

A `path` is a node-index path into `document.nodes`: the first segment indexes the top-level nodes, each later segment indexes into that element's children. Patch operations are `replace`, `insert`, `remove` (each takes a `path`), plus `meta`, `frontmatter`, and `data` merges. The same context API powers `@comark/react`, `@comark/svelte`, and `@comark/angular`: websocket handlers, agents, devtools, and HMR all drive it the same way.

---

## Component bindings

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

### Prop binding

Attributes in Comark syntax are passed as props to your component. Use the `:` prefix to pass typed values:

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

### Named slots

Named slots in Comark (`#slotname`) map to Vue named slots:

- **Default slot** → `<slot />`
- **Named slots** → `<slot name="slotname" />` (for example, `#footer` → `<slot name="footer" />`)

<code-group>
```vue [Vue]
<script setup lang="ts">
defineProps<{
  title?: string
}>()
</script>

<template>
  <div class="card">
    <h3 v-if="title">{{ title }}</h3>
    <slot />
    <footer>
      <slot name="footer" />
    </footer>
  </div>
</template>
```


```mdc [Comark]
::card{title="My Card"}
Default slot content.

#footer
Footer slot content.
::
```
</code-group>

---

## Overriding HTML elements

Override how native HTML elements render by mapping a component to their tag name via the `components` prop in both the `Markdown` and `MarkdownDocument` components.

<steps level="3">
### Create overridden version


```vue [components/Heading.vue]
<script setup lang="ts">
const props = defineProps<{
  __node?: ElementNode
  id?: string
}>()
</script>

<template>
  <component :is="__node?.[0] || 'h2'" :id="id" class="heading">
    <a v-if="id" :href="`#${id}`" class="anchor">#</a>
    <slot />
  </component>
</template>
```


### Map


Pass the component to the `components` prop:


```vue [App.vue]
<template>
  <Markdown :components="{ h1: Heading, h2: Heading, h3: Heading }">
    {{ content }}
  </Markdown>
</template>
```
</steps>

### Resolution order

When Comark encounters a tag, it looks for a matching Vue component in this order, stopping at the first match:

1. **`Prose{PascalTag}`**: for example, `ProseH1` for `h1`. Follows the Nuxt Content prose component convention.
2. **`{PascalTag}`**: for example, `Alert` for `alert`. PascalCase version of the tag name.
3. **`{tag}`**: for example, `alert`. Exact tag name as-is.
4. **Global**: any component registered via `app.component()`.

If none match, the tag renders as a native HTML element.

---

## Nuxt UI integration

When [`@nuxt/ui`](https://ui.nuxt.com) is installed, Comark automatically uses Nuxt UI's prose components for enhanced styling.

### Vite setup

Enable prose components by setting `prose: true` in the Nuxt UI Vite plugin:

```typescript [vite.config.ts]
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'

export default defineConfig({
  plugins: [
    vue(),
    ui({
      prose: true
    })
  ]
})
```

### CSS setup

```css [src/assets/css/main.css]
@import "tailwindcss";
@import "@nuxt/ui";
```

---

## Streaming

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

### Setup

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

```vue [components/AiChat.vue]
<script setup lang="ts">
import { ref } from 'vue'
import { Markdown } from '@comark/vue'

const content = ref('')
const isStreaming = ref(false)

async function askAI(prompt: string) {
  content.value = ''
  isStreaming.value = 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
    content.value += decoder.decode(value, { stream: true })
  }

  isStreaming.value = false
}
</script>

<template>
  <Markdown :streaming="isStreaming" caret>
    {{ content }}
  </Markdown>
</template>
```

<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` prop appends a blinking cursor to the last text node while `streaming` is `true`:

```vue [App.vue]
<!-- Default caret -->
<Markdown :streaming="isStreaming" caret>{{ content }}</Markdown>

<!-- Custom caret class -->
<Markdown :streaming="isStreaming" :caret="{ class: 'my-caret' }">{{ content }}</Markdown>
```

```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` prop in components that override HTML elements:

```vue [ComarkWrapper.vue]
<script setup lang="ts">
import type { Component } from 'vue'
import type { ComarkPlugin } from 'comark'
import { Markdown } from '@comark/vue'

interface Props {
  content: string
  components?: Record<string, Component>
  plugins?: ComarkPlugin[]
}

const props = defineProps<Props>()
</script>

<template>
  <Suspense>
    <Markdown :components="props.components" :plugins="props.plugins">
      {{ props.content }}
    </Markdown>
  </Suspense>
</template>
```

```vue [components/Heading.vue]
<script setup lang="ts">
import type { ElementNode } from 'comark'

defineProps<{
  __node?: ElementNode
  id?: string
}>()
</script>
```

---

---

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