Rendering

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

pnpm 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>
For server-side rendering, use the ComarkRenderer component combined with parsing on the server instead.
App.vue
<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>

Props

PropTypeDefaultDescription
markdownstringundefinedAlternative to default slot
optionsParseOptions{}Parser options (autoUnwrap, autoClose, etc.)
pluginsComarkPlugin[][]Array of plugins
componentsRecord<string, Component>{}Custom Vue component mappings
componentsManifestComponentManifestundefinedDynamic component resolver
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.vue
<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.

App.vue
<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:

App.vue
<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:

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:

App.vue
<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>
Components receive props from the markdown and render children via slots.

Use it in your Markdown content

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

componentsManifest

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

App.vue
<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.

App.vue
<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

comark.ts
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

App.vue
<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

OptionTypeDefaultDescription
extendsReturnType<typeof defineComarkComponent>undefinedInherit plugins and components from another component
namestringundefinedComponent name for debugging
autoUnwrapbooleantrueAutomatically unwrap single block elements
autoClosebooleantrueAuto-close incomplete markdown syntax
htmlbooleantrueParse embedded HTML tags into AST nodes
pluginsComarkPlugin[][]Array of plugins
componentsRecord<string, Component>{}Custom Vue component mappings
classstringundefinedAdditional CSS classes for the wrapper div

extends

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

comark.ts
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

server.ts
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

ContentPage.vue
<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

PropTypeDefaultDescription
treeComarkTreeRequired. The parsed tree returned by parse()
componentsRecord<string, Component>{}Custom Vue component mappings
componentsManifestComponentManifestundefinedDynamic component resolver for lazy-loaded components
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"

defineComarkRendererComponent

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

Setup

Expose your configured renderer

comark.ts
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

ArticlePage.vue
<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

OptionTypeDefaultDescription
extendsReturnType<typeof defineComarkRendererComponent>undefinedInherit component mappings from another renderer
namestringundefinedComponent name for debugging
componentsRecord<string, Component>{}Custom Vue component mappings
classstringundefinedAdditional CSS classes for the wrapper div

Inheritance

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

comark.ts
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:

MarkdownProp 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>

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

components/Heading.vue
<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:

App.vue
<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:

  1. Prose{PascalTag} — e.g., ProseH1 for h1. Follows the Nuxt Content prose component convention.
  2. {PascalTag} — e.g., Alert for alert. PascalCase version of the tag name.
  3. {tag} — e.g., 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 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:

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

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:

components/AiChat.vue
<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:

App.vue
<!-- 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:

ComarkWrapper.vue
<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>
components/Heading.vue
<script setup lang="ts">
import type { ComarkElement } from 'comark'

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