Comark vs vue-markdown-render

Compare Comark with vue-markdown-render and @crazydos/vue-markdown for Vue 3. See how each renders Markdown, maps elements to Vue components, handles raw HTML, and streams AI output.

TL;DR: vue-markdown-render is a small wrapper that renders markdown-it HTML into a <div>. @crazydos/vue-markdown brings the react-markdown model to Vue, with unified plugins and scoped slots per tag. Comark's @comark/vue renders Vue components from a component syntax in the content, streams AI output, and has a Nuxt module.

At a glance

Comarkvue-markdown-render@crazydos/vue-markdown
Parsermarkdown-exit (markdown-it plugin API)markdown-it 14unified (remark and rehype)
OutputVue VNodesHTML string set as innerHTMLVue VNodes
Restyle elementscomponents map, Prose prefixmarkdown-it pluginsScoped slots per tag, customAttrs
Vue components in the content::alert{type="info"} syntaxNo<slot slot-name="..."> with rehype-raw and sanitize off
GFM tables and task listsBuilt inTables built in, task lists via pluginVia remark-gfm
Raw HTML defaultParsed into nodesEscaped (html: false)Shown as text until you add rehype-raw
Streamingstreaming and caret props, autoCloseNoNo
Async rendering<Markdown> is async, wrap in <Suspense>SynchronousVueMarkdownAsync for async plugins
PluginsComark plugins, markdown-it pluginsmarkdown-it pluginsremark and rehype plugins

Three rendering models

vue-markdown-render calls markdown-it and sets the result as the innerHTML of a <div>. It accepts markdown-it options and plugins:

vue-markdown-render
<script setup lang="ts">
import VueMarkdown from 'vue-markdown-render'
import MarkdownItAnchor from 'markdown-it-anchor'

const plugins = [MarkdownItAnchor]
</script>

<template>
  <vue-markdown :source="markdown" :plugins="plugins" />
</template>

Because the output is an HTML string, Vue can't mount components inside it. It inherits markdown-it's safe defaults: raw HTML is off, and validateLink rejects javascript: and similar URLs. If you set html: true, markdown-it recommends an external sanitizer.

@crazydos/vue-markdown renders VNodes from a unified pipeline. You replace an element with a scoped slot, and you render its children with <component :is>:

@crazydos/vue-markdown
<template>
  <VueMarkdown :markdown="markdown">
    <template #h2="{ children, ...props }">
      <h2 v-bind="props" class="title">
        <component :is="children" />
      </h2>
    </template>
  </VueMarkdown>
</template>

Comark parses to a document and renders each node as a VNode. The components prop maps a tag to a Vue component:

Comark
<script setup lang="ts">
import { Markdown } from '@comark/vue'
import Heading from './components/Heading.vue'
</script>

<template>
  <Suspense>
    <Markdown :value="markdown" :components="{ h2: Heading }" />
  </Suspense>
</template>

Comark looks up ProseH2, then H2, then h2, then globally registered components. In Nuxt, the @comark/nuxt module registers the components in ~/components/prose for you.

Components in the content

@crazydos/vue-markdown supports a <slot slot-name="..."> tag in the Markdown, filled from the parent with <template #s-name>. It requires rehype-raw and :sanitize="false", and it doesn't support Vue template syntax such as v-bind.

Comark makes components part of the syntax. Props, typed values, and named slots are text, and the renderer maps each #slot to a Vue named slot:

content.md
::card{title="Release notes" :version="2"}
The new parser is live.

#footer
Read the [changelog](https://example.com/changelog).
::
components/Card.vue
<script setup lang="ts">
defineProps<{ title?: string, version?: number }>()
</script>

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

Streaming

Neither Vue alternative documents a streaming mode. Comark's renderer accepts the growing text and completes unterminated syntax with autoClose:

Message.vue
<template>
  <Suspense>
    <Markdown :value="part.text" :streaming="part.state === 'streaming'" caret />
  </Suspense>
</template>

The part object comes from @ai-sdk/vue. See the Nuxt AI SDK example and streaming in Vue.

What the alternatives do well

  • vue-markdown-render is small and synchronous: it adds little on top of markdown-it and needs no <Suspense>. Comark's parser bundle is larger, because it includes markdown-exit, a YAML parser, an HTML parser, and entity tables.
  • @crazydos/vue-markdown uses unified: remark and rehype plugins work as-is. Comark has no remark or rehype plugin support.
  • Safe raw HTML handling: both escape or hide raw HTML by default. Comark parses raw HTML and keeps <script>, so add the security plugin with a blockedTags list for untrusted input.

If you render trusted, static Markdown and don't need components, vue-markdown-render is enough. If you rely on remark plugins, @crazydos/vue-markdown fits. Choose Comark for components in the content, streaming, or a Nuxt integration.

FAQ

© 2026 Vercel, Inc.