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'
</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 |
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 — 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 },
})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>