Comark vs vue-markdown-render
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
| Comark | vue-markdown-render | @crazydos/vue-markdown | |
|---|---|---|---|
| Parser | markdown-exit (markdown-it plugin API) | markdown-it 14 | unified (remark and rehype) |
| Output | Vue VNodes | HTML string set as innerHTML | Vue VNodes |
| Restyle elements | components map, Prose prefix | markdown-it plugins | Scoped slots per tag, customAttrs |
| Vue components in the content | ::alert{type="info"} syntax | No | <slot slot-name="..."> with rehype-raw and sanitize off |
| GFM tables and task lists | Built in | Tables built in, task lists via plugin | Via remark-gfm |
| Raw HTML default | Parsed into nodes | Escaped (html: false) | Shown as text until you add rehype-raw |
| Streaming | streaming and caret props, autoClose | No | No |
| Async rendering | <Markdown> is async, wrap in <Suspense> | Synchronous | VueMarkdownAsync for async plugins |
| Plugins | Comark plugins, markdown-it plugins | markdown-it plugins | remark 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:
<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>:
<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:
<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:
::card{title="Release notes" :version="2"}
The new parser is live.
#footer
Read the [changelog](https://example.com/changelog).
::<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:
<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 ablockedTagslist 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
defineComarkPlugin and pass it in markdownItPlugins. See markdown-it plugins.<Markdown> parses asynchronously. To skip parsing in the browser, parse on the server and render the document with <MarkdownDocument>. It ships no parser to the client. See MarkdownDocument.@comark/nuxt module auto-imports the components and registers prose overrides. It uses Nuxt UI prose components when @nuxt/ui is installed. See Nuxt.javascript: URL doesn't become a link, and the Vue renderer never forwards innerHTML, but the default html plugin keeps <script>. Add security() from @comark/vue/plugins/security with a blockedTags list.