Vue Markdown Editor: Build One in 30 Lines (2026)
September 11, 2026 · 9 min read
Vue Markdown Editor: Add Markdown Editing to a Vue App
A vue markdown editor needs three things. A ref holds the text, a computed renders it, and a v-html pane shows the result. With markdown-it and DOMPurify that's about 30 lines of Vue 3. This tutorial builds that version first, then compares the packaged editors (md-editor-v3, v-md-editor, vue3-markdown, Tiptap) for when you need a toolbar and image upload.
How Does the Official Vue Markdown Example Work?
The Vue docs ship a Markdown editor example that's been around since Vue 2. Its whole logic is a ref('# hello'), a computed(() => marked(input.value)), and a debounced @input handler on a textarea. The template has two elements: the textarea bound to input, and a div with v-html="output".
That's the right shape, and everything below keeps it. Two things need changing for production, though. The official example doesn't sanitize the HTML before v-html, and Vue's own security guide says to use v-html only when you know the HTML is safe.
It also uses marked, which is fine, but markdown-it's plugin system and CommonMark compliance make it the parser we prefer for editors. If you want the background on what either parser does, the Markdown to HTML guide covers it. This post treats the parser as a black box.
Build a 30-Line Vue 3 Markdown Editor
Install the two dependencies. markdown-it 15.0.2 and DOMPurify 3.4.15 are current at the time of writing.
npm install markdown-it dompurify
Then create MarkdownEditor.vue with the Composition API:
<script setup>
import { ref, computed } from 'vue'
import MarkdownIt from 'markdown-it'
import DOMPurify from 'dompurify'
const md = new MarkdownIt({ linkify: true })
const source = ref(`# Release notes
Version **2.1** ships two changes:
- Faster export
- A new [changelog page](https://example.com/changelog)
`)
const html = computed(() => DOMPurify.sanitize(md.render(source.value)))
</script>
<template>
<div class="editor">
<textarea v-model="source" spellcheck="false"></textarea>
<div class="preview" v-html="html"></div>
</div>
</template>
<style scoped>
.editor { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; height: 70vh; }
textarea { font: 14px/1.5 monospace; padding: 1rem; resize: none; }
.preview { overflow: auto; padding: 1rem; border: 1px solid #ddd; }
</style>
v-model on the textarea replaces the official example's debounce. Re-rendering on every keystroke is fine for documents of a few thousand words; markdown-it parses a 10 KB file in a couple of milliseconds. Add a debounce only if you measure a problem.
The computed re-runs whenever source changes and caches otherwise, so the preview never renders more than once per edit. Drop the component into App.vue and you have a working two-pane editor.
Here's the same sample in our editor so you can compare the rendered output with what your component produces.
If the table and task list above render here but not in your component, that's expected. GFM tables are on by default in markdown-it, but task lists need the markdown-it-task-lists plugin.
Sanitizing v-html Output Before It Renders
v-html inserts whatever string it's given. A user who types <img src=x onerror="alert(1)"> into your textarea gets a script running in every other user's browser once the Markdown is stored and shown. Two settings close that hole, and we use both.
First, markdown-it's html option defaults to false. In our test, new MarkdownIt().render('<script>alert(1)</script>') returned the tags escaped as <script>, so raw HTML in the Markdown is shown as text rather than executed. Leave it off unless you need inline HTML.
Second, DOMPurify sanitizes the rendered output. It matters even with html: false, because links can carry javascript: URLs and plugins can emit markup you didn't audit. DOMPurify.sanitize(html) strips event handlers, scripts, and unsafe URLs while keeping ordinary tags.
If you do need safe HTML (say, <kbd> or <details> in docs), set html: true in markdown-it and let DOMPurify be the gate.
const md = new MarkdownIt({ html: true, linkify: true })
const html = computed(() =>
DOMPurify.sanitize(md.render(source.value), {
ADD_TAGS: ['details', 'summary'],
})
)
Vue's guide adds one more rule: sanitize on the server before the Markdown is saved, not only in the browser. The client-side pass protects the preview; the server-side pass protects everyone else.
Syntax Highlighting and Plugins
markdown-it's highlight option receives the code and the language tag from each fenced block. Wire it to highlight.js and any block with a language identifier gets colour.
import hljs from 'highlight.js'
const md = new MarkdownIt({
highlight(str, lang) {
if (lang && hljs.getLanguage(lang)) {
return hljs.highlight(str, { language: lang }).value
}
return ''
},
})
Returning an empty string tells markdown-it to fall back to its own escaping, which is what you want for unknown languages. The code block languages guide lists the identifiers highlight.js and Prism recognise.
Plugins chain with .use(). The ones we reach for are markdown-it-task-lists (checkboxes), markdown-it-footnote, and markdown-it-anchor for heading IDs. For math and diagrams, the packaged editors below bundle KaTeX and Mermaid, which is often the reason to switch to one. Our equation and Mermaid guides show what those look like when they work.
Which Packaged Vue Markdown Editor Should You Pick?
The hand-built component has no toolbar, no image upload, and no split-pane scroll sync. If users expect those, a package saves weeks. Here's how the five names that come up in every search compare, checked against the npm registry on 11 September 2026.
| Package | Vue version | Latest release | Licence | Notes |
|---|---|---|---|---|
| md-editor-v3 | Vue 3 (peer ^3.5.3) | 6.5.6, August 2026 | MIT | TSX, CodeMirror 6, toolbar, themes, Mermaid and KaTeX, SSR since 1.6 |
| @kangc/v-md-editor | Vue 2 (latest), Vue 3 (@next) | 1.7.12 and 2.3.18, both November 2023 | MIT | Themes, plugins; no release in almost three years |
| vue3-markdown | Vue 3 | 1.2.17, September 2025 | Not declared on npm | micromark-based, ships DOMPurify, editor plus preview |
| @tiptap/vue-3 with @tiptap/markdown | Vue 3 | 3.31.3, September 2026 | MIT | WYSIWYG, not a split pane; Markdown import and export |
| Kendo UI for Vue Editor | Vue 3 | Commercial | Commercial | Rich text editor with a Markdown mode; needs a licence |
md-editor-v3 is the default choice for a Vue 3 split-pane editor, and searches for a Vue md-editor almost always end up there. Install with npm i md-editor-v3, import the stylesheet, and bind with v-model.
<script setup>
import { ref } from 'vue'
import { MdEditor } from 'md-editor-v3'
import 'md-editor-v3/lib/style.css'
const text = ref('# Hello Editor')
</script>
<template>
<MdEditor v-model="text" language="en-US" />
</template>
The MdPreview component renders without an editor for read-only pages, and the README notes that its default language is Chinese, hence the language prop. The @vavt/cm-extension and @vavt/v3-extension packages add languages and toolbar items such as PDF export.
v-md-editor from code-farmer-i (the GitHub repo is named vue-markdown-editor) was the popular Vue 2 option. Its Vue 3 build lives behind the @next tag, but neither line has had a release since November 2023, so treat it as maintenance-only. vue3-markdown is lighter and bundles DOMPurify, which is a sensible default. Tiptap is a different category: it's a WYSIWYG editor that can read and write Markdown, covered in our Tiptap Markdown guide.
One limitation applies to all of them: every packaged editor bundles its own parser, so its rendering can differ from GitHub's or your static site's. Check headings, tables, and line breaks in the target renderer before you ship.
Using a Vue Markdown Editor in Nuxt
Editors touch window, document, and the DOM on mount, and every packaged option above does. Under server-side rendering that throws. Nuxt's <ClientOnly> component renders its children only in the browser and accepts a #fallback slot for the server-rendered placeholder.
<template>
<ClientOnly>
<MdEditor v-model="text" />
<template #fallback>
<p>Loading editor...</p>
</template>
</ClientOnly>
</template>
md-editor-v3's README also asks you to set a constant id prop under SSR so the server and client markup match. For pages that only display Markdown (docs, blog posts), skip the editor entirely and use @nuxt/content, which parses .md files at build time and needs no client-side parser.
If you're weighing Vue against another framework for the same feature, the React Markdown and Angular ngx-markdown tutorials build the equivalent component in each.
Common Vue Markdown Editor Mistakes
Skipping sanitization because the editor is internal. Stored Markdown outlives the audience you had in mind. Sanitize with DOMPurify on the client and again on the server before saving.
Re-rendering the whole document on every keystroke with a slow parser. markdown-it is fast enough for most content. If you enable heavy plugins (Mermaid rendering on every change is the usual culprit), debounce the input or move diagram rendering to a watch with a delay.
Importing a packaged editor at the top level of a Nuxt page. The import alone can reference window and crash SSR. Wrap the component in <ClientOnly> or load it with defineAsyncComponent inside onMounted.
Vue Markdown Editor FAQ
A vue markdown editor starts as a ref, a computed, and a sanitized v-html, and that version is enough for comments, notes, and admin forms. Reach for md-editor-v3 when users need a toolbar and uploads, and wrap either in <ClientOnly> under Nuxt. To check how your component's output compares with a standard renderer, paste the same Markdown into the editor.