Render Comark in Vue
The @comark/vue package provides Vue components for rendering Comark content with full support for custom components, plugins, and streaming.
Installation
pnpm add @comark/vuenpm install @comark/vueyarn add @comark/vuebun add @comark/vue<Comark>
The <Comark> component is the simplest way to render markdown in Vue. It handles parsing and rendering automatically.
<Comark> is an async component and must always be wrapped in <Suspense>ComarkRenderer component combined with parsing on the server instead.<script setup lang="ts">
import { Comark } from '@comark/vue'
const content = `# Hello World
This is **markdown** with Comark components.
`
</script>
<template>
<Suspense>
<Comark>{{ content }}</Comark>
</Suspense>
</template>Usage
Pass markdown content via the default slot or the markdown prop:
<template>
<Comark>{{ content }}</Comark>
</template><template>
<Comark :markdown="content" />
</template>Props
| Prop | Type | Default | Description |
|---|---|---|---|
markdown | string | undefined | Alternative to default slot |
options | ParseOptions | {} | Parser options (autoUnwrap, autoClose, etc.) |
plugins | ComarkPlugin[] | [] | Array of plugins |
components | Record<string, Component> | {} | Custom Vue component mappings |
componentsManifest | ComponentManifest | undefined | Dynamic component resolver |
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.
<script setup lang="ts">
import { Comark } from '@comark/vue'
</script>
<template>
<Comark :options="{ autoUnwrap: true, autoClose: true }">
{{ content }}
</Comark>
</template>plugins
See ComarkPlugin for available plugins.
<script setup lang="ts">
import { Comark } from '@comark/vue'
import highlight from '@comark/vue/plugins/highlight'
import githubLight from '@shikijs/themes/github-light'
import githubDark from '@shikijs/themes/github-dark'
const plugins = [
highlight({
themes: {
light: githubLight,
dark: githubDark
}
})
]
</script>
<template>
<Suspense>
<Comark :plugins="plugins">{{ content }}</Comark>
</Suspense>
</template>For math and mermaid plugins, also pass the companion components:
<script setup lang="ts">
import { Comark } 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>
<Comark
:markdown="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.
Create a Vue component
Save a component such as 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 Comark:
<script setup lang="ts">
import { Comark } from '@comark/vue'
import Alert from './components/Alert.vue'
import Card from './components/Card.vue'
const components = { alert: Alert, card: Card }
</script>
<template>
<Comark :components="components">{{ content }}</Comark>
</template>Use it in your Markdown content
::alert{type="warning"}
This is a warning message!
::componentsManifest
For lazy-loading components on demand. Components are resolved once and cached. Works with both <Comark> and <ComarkRenderer>:
<script setup lang="ts">
import { Comark } from '@comark/vue'
const manifest = (name: string) => {
return import(`./components/prose/${name}.vue`)
}
</script>
<template>
<Comark :components-manifest="manifest">{{ content }}</Comark>
</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 for the full scope.
<script setup lang="ts">
import { Comark } from '@comark/vue'
const user = { name: 'Ada', role: 'admin' }
const content = `Hello, :badge{:label="data.user.name"}!`
</script>
<template>
<Suspense>
<Comark :markdown="content" :data="{ user }" />
</Suspense>
</template>defineComarkComponent
Creates a pre-configured <Comark> component with default options, plugins, and components baked in.
Usage
Expose your configured component
import { defineComarkComponent } from '@comark/vue'
import highlight from '@comark/vue/plugins/highlight'
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 AppComark = defineComarkComponent({
name: 'AppComark',
plugins: [
math(),
mermaid(),
highlight({
themes: {
light: githubLight,
dark: githubDark
},
}),
],
components: {
Math,
Mermaid,
alert: CustomAlert,
},
})Use it in your templates
<script setup lang="ts">
import { AppComark } from './comark'
</script>
<template>
<!-- All configuration is already included -->
<AppComark>{{ content }}</AppComark>
<!-- Can still override per-instance -->
<AppComark :components="{ alert: DifferentAlert }">{{ content }}</AppComark>
</template>Options
| Option | Type | Default | Description |
|---|---|---|---|
extends | ReturnType<typeof defineComarkComponent> | 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 |
html | boolean | true | Parse embedded HTML tags into AST nodes |
linkify | boolean | true | Auto-convert URL-like text into links |
plugins | ComarkPlugin[] | [] | Array of plugins |
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:
import { defineComarkComponent } from '@comark/vue'
import highlight from '@comark/vue/plugins/highlight'
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 BaseComark = defineComarkComponent({
name: 'BaseComark',
plugins: [
highlight({ themes: { light: githubLight, dark: githubDark } }),
],
components: { ProsePre },
})
// Article: extends Base, adds TOC and math
export const ArticleComark = defineComarkComponent({
name: 'ArticleComark',
extends: BaseComark,
plugins: [toc({ depth: 3 }), math()],
components: { Math },
})
// Comment: extends Base only, no TOC, no math
export const CommentComark = defineComarkComponent({
name: 'CommentComark',
extends: BaseComark,
})Merging behavior
plugins: Arrays are concatenated (config plugins + prop plugins)components: Component mappings override global configuration- Other
options: Component options override global configuration
<ComarkRenderer>
Renders a pre-parsed ComarkTree 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 on the server
import { createParse } from 'comark'
import { readFile } from 'node:fs/promises'
const parse = createParse()
// In your server handler
export async function getContentTree(slug: string) {
const markdown = await readFile(`content/${slug}.md`, 'utf-8')
return parse(markdown)
}Render with ComarkRenderer
<script setup lang="ts">
import { ComarkRenderer } from '@comark/vue'
import Alert from './components/Alert.vue'
const { slug } = defineProps<{ slug: string }>()
const res = await fetch(`/api/content/${slug}`)
const tree = await res.json()
</script>
<template>
<ComarkRenderer :tree="tree" :components="{ alert: Alert }" />
</template>Renderer Props
| Prop | Type | Default | Description |
|---|---|---|---|
tree | ComarkTree | — | Required. The parsed tree returned by parse() |
components | Record<string, Component> | {} | Custom Vue component mappings |
componentsManifest | ComponentManifest | undefined | Dynamic component resolver for lazy-loaded components |
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" |
defineComarkRendererComponent
Creates a pre-configured <ComarkRenderer> with baked-in component mappings.
Setup
Expose your configured renderer
import { defineComarkRendererComponent } from '@comark/vue'
import CustomAlert from './components/Alert.vue'
import ProsePre from './components/ProsePre.vue'
export const ArticleRenderer = defineComarkRendererComponent({
name: 'ArticleRenderer',
components: {
alert: CustomAlert,
ProsePre,
},
})Use it in your templates
<script setup lang="ts">
import { ArticleRenderer } from './comark'
const { slug } = defineProps<{ slug: string }>()
const res = await fetch(`/api/article/${slug}`)
const tree = await res.json()
</script>
<template>
<ArticleRenderer :tree="tree" />
</template>Renderer Options
| Option | Type | Default | Description |
|---|---|---|---|
extends | ReturnType<typeof defineComarkRendererComponent> | undefined | Inherit component mappings from another renderer |
name | string | undefined | Component name for debugging |
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:
import { defineComarkRendererComponent } 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 BaseRenderer = defineComarkRendererComponent({
name: 'BaseRenderer',
components: { ProsePre, ProseA },
})
export const ArticleRenderer = defineComarkRendererComponent({
name: 'ArticleRenderer',
extends: BaseRenderer,
components: { alert: CustomAlert },
})
export const CommentRenderer = defineComarkRendererComponent({
name: 'CommentRenderer',
extends: BaseRenderer,
components: { alert: CommentAlert },
})Live Documents
ComarkRenderer 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 tree's own meta.key (set by a plugin) or the comark-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.
<template>
<ComarkRenderer comark-key="page" :tree="tree" />
</template>A driver installs the context once and pushes updates by key with set() (replace the whole tree) or patch() (surgical node edits, with structural sharing so only the changed branch re-renders):
import { createComarkContext, parse } from 'comark'
const ctx = createComarkContext() // installs globalThis.comarkContext
const doc = ctx.get('page', await parse('# Hello')) // seed on first access
doc.set(await parse('# Replaced'))
doc.patch({ op: 'insert', path: [1], node: ['p', {}, 'inserted'] })A path is a node-index path into tree.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" />(e.g.,#footer→<slot name="footer" />)
<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>::card{title="My Card"}
Default slot content.
#footer
Footer slot content.
::Overriding HTML Elements
Override how native HTML elements render by mapping a component to their tag name via the components prop in both the Comark and ComarkRenderer components.
Create overridden version
<script setup lang="ts">
const props = defineProps<{
__node?: ComarkElement
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:
<template>
<Comark :components="{ h1: Heading, h2: Heading, h3: Heading }">
{{ content }}
</Comark>
</template>Resolution Order
When Comark encounters a tag, it looks for a matching Vue component in this order, stopping at the first match:
Prose{PascalTag}: e.g.,ProseH1forh1. Follows the Nuxt Content prose component convention.{PascalTag}: e.g.,Alertforalert. PascalCase version of the tag name.{tag}: e.g.,alert. Exact tag name as-is.- Global: any component registered via
app.component().
If none match, the tag renders as a native HTML element.
Nuxt UI Integration
When @nuxt/ui 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:
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
@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:
<script setup lang="ts">
import { ref } from 'vue'
import { Comark } 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>
<Comark :streaming="isStreaming" caret>
{{ content }}
</Comark>
</template>autoClose is enabled by default: incomplete syntax like **bold text is automatically closed on every parse. Disable with :options="{ autoClose: false }".Caret
The caret prop appends a blinking cursor to the last text node while streaming is true:
<!-- Default caret -->
<Comark :streaming="isStreaming" caret>{{ content }}</Comark>
<!-- Custom caret class -->
<Comark :streaming="isStreaming" :caret="{ class: 'my-caret' }">{{ content }}</Comark>.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 prop in components that override HTML elements:
<script setup lang="ts">
import type { Component } from 'vue'
import type { ComarkPlugin } from 'comark'
import { Comark } from '@comark/vue'
interface Props {
content: string
components?: Record<string, Component>
plugins?: ComarkPlugin[]
}
const props = defineProps<Props>()
</script>
<template>
<Suspense>
<Comark :components="props.components" :plugins="props.plugins">
{{ props.content }}
</Comark>
</Suspense>
</template><script setup lang="ts">
import type { ComarkElement } from 'comark'
defineProps<{
__node?: ComarkElement
id?: string
}>()
</script>