nbeditor

A reusable Vue 3 markdown editor: CodeMirror 6 editing, live preview, Mermaid diagrams, KaTeX math, and Markdown / Word / PDF export.
631
Install
npm install nbeditor
Latest Version:1.1.3
Vue:3
License:MIT
Last Updated:Oct 20, 2026
Links: npm
Maintainer: nbbruce

nbeditor

The production-ready Markdown workspace for Vue 3. Drop a polished writing surface into your product with CodeMirror 6 editing, instant preview, interactive Mermaid diagrams, KaTeX mathematics, attachments, autosave, and genuine .docx / PDF exports. nbeditor turns a blank page into a documentation editor, knowledge base, report composer, or embedded authoring experience in minutes.

It is built for the last mile that usually consumes weeks: faithful exports, page-aware diagrams, portable attachments, safe rendering, and an API that remains easy to customize when your product outgrows the default toolbar.

npm version npm downloads license types

npm i nbeditor

Why teams choose nbeditor

Because every other markdown editor stops at the <textarea> and leaves you to build the export pipeline yourself — that's the part that actually hurts. Getting math to survive a .docx round trip, getting a Mermaid diagram to not blow up a printed page, getting a code table to not wallpaper four blank sheets of paper: that's days of work nobody wants to do twice.

nbeditor ships that pipeline in one focused package, with a ready-to-use interface and headless utilities you can compose into your own product. It feels like a complete editor on day one and stays pleasant to integrate as your application grows.

A complete writing surface, not a textarea wrapper. Give users keyboard-friendly CodeMirror 6 editing, Markdown formatting actions, split-pane preview, synchronized scrolling, themes, status information, autosave, import, and export from the first render.

Attachments for prototypes and production. Configure image and file uploads with an endpoint or a custom (file) => Promise<string> adapter. With no endpoint, files become portable data: URLs, so the editor remains useful on a static site with no server write path.

Deploy the playground anywhere. Build a static directory with relative asset paths and publish it to a CDN, object storage website, Nginx host, or shared hosting provider.

1. Exports that are real files, not markup in a trench coat. Word output is a genuine OOXML .docx built with the docx library — H1–H6 mapped to real Word heading styles, styled blockquotes with a left rule, bullet and ordered numbering, task lists with ☑/☐ glyphs, GFM tables with per-column widths, shaded Consolas code blocks, horizontal rules, and Mermaid diagrams embedded as vector SVG with a canvg-rasterized PNG fallback for Word builds that refuse SVG. Not "HTML saved with a .doc extension" that Word opens in a mangled compatibility mode.

2. PDF that paginates properly, with zero dependencies. No paged.js, no 300 KB pagination engine, no canvas rasterizer. nbeditor renders into a scoped off-screen print root, fits every diagram to the printable page box, injects a real @media print stylesheet, and hands off to the browser's own print engine. You get selectable text, real vector Mermaid diagrams, embedded KaTeX webfonts, and correct page breaks — window.print() → Save as PDF.

3. Diagrams that respect the page. This is the bug that eats every "markdown to PDF" project: an erDiagram renders at 148 × 447 px, someone slaps max-width: 100% on it, it stretches to 607 px wide → 1832 px tall → an SVG can't be split across pages → you get a blank page plus a truncated diagram. nbeditor measures each diagram against the actual printable box (derived from page size minus margins), scales it to at most 90% of the printable height preserving aspect ratio, pins break-inside: avoid, and normalizes tables and code blocks for pagination. Word export uses the same fit-to-box math, so a tall diagram can never overflow a page there either.

4. Math that survives. KaTeX for $inline$ and $$display$$, with a custom tokenizer that is currency-safe ($5 and $10 stays $5 and $10) and code-block protected. KaTeX's stylesheet is deliberately not inlined into the bundle so your bundler emits the math webfonts as separate, cacheable files instead of a 1.4 MB CSS blob.

5. Framework-agnostic core. Only Vue is a peer dependency. The renderer, the Mermaid pipeline and both exporters are plain functions you can call from React, Svelte, vanilla JS, a Node script or a Nuxt server route. Use the drop-in <NbEditor />, or wire the headless API into a shell you already have.

6. Not a black box. Tree-shakeable ESM + CJS, first-class TypeScript types, pluggable logger for error telemetry, and namespaced nb- / nbeditor- classes with --nb-* CSS variables. Nothing leaks into your app.

How it compares

nbeditor Typical editor component DIY markdown-it Print-a-HTML-page hack
Editor UI included Yes (CodeMirror 6) Yes No No
Live preview + sync scroll Yes Yes No No
Mermaid, interactive + cached Yes Sometimes No Renders as <pre>
KaTeX Yes Sometimes Manual Manual
Real .docx export Yes (docx lib) Rarely No No — fake .doc
Mermaid inside .docx SVG + PNG fallback No No No
PDF pagination Native print engine Iframe screenshot No Blank pages, cut diagrams
Diagram page-fitting Measured per page box No No No
Bundled pagination engine None Varies None paged.js (~300 KB)
Headless API Yes No N/A No
Framework lock-in Vue peer only Vue/React variant None None

Quick start

npm i nbeditor vue katex
<script setup lang="ts">
  import { ref } from 'vue'
  import { NbEditor } from 'nbeditor'

  import 'nbeditor/style.css' // editor layout, preview typography, Mermaid UI
  import 'katex/dist/katex.min.css' // math stylesheet + webfonts

  const markdown = ref('# Hello\n\n$E = mc^2$\n')
</script>

<template>
  <NbEditor
    v-model="markdown"
    theme="dark"
    storage-key="my-doc"
    filename="my-doc"
    page-size="A4"
    margins="normal"
    height="100vh"
  />
</template>

That's it: a full editor with toolbar, split panes, autosave, status bar, Mermaid zoom/pan/modal, and working Markdown / Word / PDF export buttons.

Import both stylesheets once at your app entry. KaTeX is intentionally kept out of nbeditor/style.css so the math webfonts stay separately cacheable.


What you get in the box

  • CodeMirror 6 editor — markdown syntax, line wrapping, one-dark / light themes, Mod-b, Mod-i, Tab indent.
  • Live preview — per-section diffing (only changed sections re-render), synchronized scrolling, debounced Mermaid.
  • Mermaid 11 — lazy loaded per diagram type, cached SVG, zoom / pan / reset / fullscreen modal.
  • KaTeX — $inline$ and $$display$$, currency-safe, code-block protected.
  • Extended markdown — emoji, footnotes, ==mark==, ~sub~, ^sup^, task lists, definition lists, GFM tables.
  • Prism.js highlighting with per-language lazy loading.
  • DOMPurify sanitization of rendered HTML and Mermaid SVG output.
  • Exports — Markdown (.md), Word (.docx), PDF (print engine).
  • Attachments — image and file actions, multipart upload endpoints, custom upload adapters, and a zero-backend data: URL fallback.
  • Static deployment — playground build with relative assets and no runtime server requirement.
  • Namespaced classes, --nb-* CSS variables, pluggable logger, full TypeScript types.

NbEditor

The batteries-included component: toolbar, split panes, status bar, autosave and all export actions.

Props

Prop Type Default Description
modelValue string '' Markdown source (v-model).
theme 'dark' | 'light' 'dark' Editor + preview theme.
height string '100%' CSS height of the editor shell.
placeholder string 'Start writing markdown…' Empty-editor placeholder.
showToolbar boolean true Render the formatting toolbar.
showPreview boolean true Show the preview pane.
showEditor boolean true Show the editor pane.
showStatusBar boolean true Show the word / char / cursor status bar.
storageKey string | null null localStorage autosave key (null disables).
filename string undefined Base filename for export actions.
debounce number 50 Preview refresh debounce (ms).
pageSize NbPageSize 'A4' PDF page size.
margins NbMarginSize 'normal' PDF margins.
imageUploadApi NbUploadApi undefined Image upload URL or (file) => Promise<string>.
allowedImageTypes string[] JPEG, PNG, GIF, WebP Allowed image MIME types for file selection, paste, and drag-and-drop.
fileUploadApi NbUploadApi undefined File upload URL or (file) => Promise<string>.

NbPageSize = 'A4' | 'Letter' | 'Legal' | 'A3' NbMarginSize = 'narrow' | 'normal' | 'wide' NbUploadApi = string | (file: File) => Promise<string>

Image upload accepts multiple files at once. Users can select several images, paste screenshots from the clipboard, or drag images into the editor; each accepted image is uploaded and inserted as its own Markdown image. Set allowedImageTypes to restrict accepted MIME types, for example:

<NbEditor
  v-model="markdown"
  :allowed-image-types="['image/png', 'image/jpeg']"
  image-upload-api="https://uploads.example.com/images"
/>

The upload URL may point to any server that accepts a multipart/form-data POST with a file field and returns JSON containing url or path (also accepted under data). Cross-origin URLs work when the upload server enables CORS for your app's origin and permits the POST request. The MIME type setting is a client-side filter for accidental or disallowed files; it is not a malware scanner or a security boundary. Validate file contents, size, and permissions on the upload server as well.

For a URL, nbeditor sends multipart/form-data with the field name file. The JSON response may contain url or path at the top level or under data.

<NbEditor
  v-model="markdown"
  image-upload-api="/api/uploads/images"
  :file-upload-api="uploadFile"
/>
const uploadFile = async (file: File): Promise<string> => {
  const form = new FormData()
  form.append('file', file)
  const response = await fetch('/api/uploads', { method: 'POST', body: form })
  const result = await response.json()
  return result.url
}

With no upload props, images are inserted as Markdown images with data: URLs and other files as Markdown download links. This is useful for prototypes and static sites; use a remote adapter for large files or shared documents.

Events

Event Payload
update:modelValue string
update:theme NbTheme
change { markdown, wordCount, charCount }
save string — emitted after a successful autosave write

Exposed methods

const editor = ref<InstanceType<typeof NbEditor> | null>(null)

editor.value?.getMarkdown()
editor.value?.setMarkdown('# replaced')
editor.value?.getEditorView() // raw CodeMirror EditorView
editor.value?.focus()
editor.value?.insertText('**bold**')
editor.value?.togglePreview()
editor.value?.toggleEditor()
editor.value?.toggleTheme()
editor.value?.exportMarkdown()
await editor.value?.exportWord()
await editor.value?.exportPdf()
<template>
  <div>
    <button @click="editor?.exportPdf()">Export PDF</button>
    <NbEditor ref="editor" v-model="markdown" :show-toolbar="false" />
  </div>
</template>

NbPreview

Render markdown without the editor — for docs sites, blog posts or read-only views.

<script setup lang="ts">
  import { ref } from 'vue'
  import { NbPreview } from 'nbeditor'
  import 'nbeditor/style.css'
  import 'katex/dist/katex.min.css'

  const markdown = ref('```mermaid\ngraph TD; A-->B\n```')
</script>

<template>
  <NbPreview :content="markdown" theme="light" height="480px" />
</template>
Prop Type Default Description
content string — Markdown source (required).
theme 'dark' | 'light' 'dark' Preview theme.
height string 'auto' CSS height of the scroll host.

Headless API

No component required. This is the part you can drop into an existing shell, a Node script or a worker.

Render markdown

import { useMarkdownRenderer } from 'nbeditor'

const { renderMarkdown, renderMarkdownFragment, renderMarkdownExample } = useMarkdownRenderer()

// Full pipeline: markdown-it → Mermaid placeholders → KaTeX → sanitized HTML
const html = await renderMarkdown('# hi\n\n$E=mc^2$\n\n```mermaid\ngraph TD;A-->B\n```')

// Per-section fragment (no Mermaid step) — what the live preview uses
const fragment = await renderMarkdownFragment('## Section')

// Plain markdown → sanitized HTML (no Mermaid / KaTeX processing)
const plain = await renderMarkdownExample('- [x] done')

renderMarkdown returns HTML with <div class="mermaid"> placeholders already in place. Call useMarkdownRenderer().renderMermaidDiagrams(config, container) afterwards to turn them into SVGs when you mount the HTML yourself.

Export Word

import { useWordExport } from 'nbeditor'

await useWordExport().exportToWord(markdown, { filename: 'my-doc' })

Consumes the markdown-it token stream directly: H1–H6, paragraphs, blockquotes, bullet and ordered lists, task lists (rendered as ☑/☐), GFM tables, fenced code blocks, horizontal rules and inline HTML (checkbox / label markup is unwrapped so task text is never dropped). Mermaid diagrams are embedded as SVG with a canvg PNG fallback; inline and display math are emitted as their LaTeX source.

Export PDF

import { usePdfExport } from 'nbeditor'

await usePdfExport().exportToPdf(markdown, {
  filename: 'my-doc',
  pageSize: 'A4',
  margins: 'normal',
  theme: 'light',
  onAfterPrint: () => console.log('print dialog closed'),
})

Renders into an off-screen .nbeditor-print-root, injects a scoped @media print stylesheet, fits Mermaid diagrams to the printable page box, temporarily sets document.title (so the print dialog suggests the right filename) and calls window.print().

  • Enable Background graphics in the print dialog to keep code-block / table backgrounds.
  • Diagrams are scaled to fit a single page (max 90% of printable height) and never split across pages.
  • No headless PDF dependency — the browser's print engine does the pagination, so text stays selectable.

Markdown file helpers

import { downloadTextFile, ensureMarkdownFilename, pickMarkdownFile } from 'nbeditor'

downloadTextFile(markdown, ensureMarkdownFilename('my-doc')) // -> my-doc.md

const file = await pickMarkdownFile() // file picker for .md / .markdown / .txt
if (file) console.log(file.filename, file.content)

Low-level utilities

Everything below is exported for advanced integrations: renderMarkdown, createMarkdownItInstance, processLatex, mathPlugin, highlightSyntax, sanitizeHtml, sanitizeSvg, updatePreviewSections, attachCodeBlockInteractions, the full Mermaid pipeline (renderMermaidDiagrams, initMermaid, clearMermaidCache, enhanceMermaidExportSvg, …) and the print style helpers (getExportPrintStyles, PAGE_SIZES, MARGIN_VALUES, PAGE_SIZE_MM, getPageContentBoxMm).


NbToolbar

Exported on its own for custom shells.

Prop Type Description
theme 'dark' | 'light' Toolbar theme.
showPreview boolean Preview toggle state.
showEditor boolean Editor toggle state.

Emits command(id), toggle-preview, toggle-editor, toggle-theme. Command ids: bold, italic, strike, highlight, code, link, h1–h3, bullet-list, ordered-list, task-list, quote, code-block, table, subscript, superscript, import, export-markdown, export-word, export-pdf.


Theming

Styling is namespaced and driven by CSS variables on the .nbeditor root. Dark is the default palette; data-theme="light" switches to the light one. Override individual tokens (or the whole palette) after importing the package stylesheet:

.nbeditor {
  --nb-bg: #0f1117;
  --nb-text: #e6e8ee;
  --nb-accent: #4f8cff;
}

Custom logger

Rendering / export errors go through a pluggable logger. Route them into your own telemetry instead of console.error:

import { setLogger } from 'nbeditor'

setLogger((context, error, meta) => {
  myReporter.captureException(error, { context, ...meta })
})

Development

npm install
npm run dev        # playground at ./playground
npm run typecheck
npm run build      # -> dist/index.mjs, dist/index.cjs, dist/nbeditor.css, dist/*.d.ts

The playground is a full editor wired to every export path — the fastest way to sanity-check a change.


Known limitations

  • Word export flattens nested lists to a single level, and does not emit footnote / definition-list blocks — those render correctly in the browser preview, they just aren't translated to OOXML yet.
  • Math inside the .docx is emitted as LaTeX source text rather than native OMML, so Word will not re-render it as an editable equation.
  • PDF export goes through the browser print dialog, so there is no headless / server-side renderer.

License

MIT © nbbruce

Related Packages

pd-editor-vue

Vue 3 Markdown editor for technical content and AI writing workflows

2,905 0
vue-stream-markdown

Streaming markdown output, Useful for text streams like LLM outputs.

447,910 243
streamdown-vue

Streamdown-style streaming Markdown renderer for Vue 3/Nuxt 3 with Shiki, KaTeX,...

333,829 53
fo-markdown-note

**fo-markdown-note** is a **Vue.js** component that provides a simple Markdown e...

1,025 1

Version History

Version Released Vue License
1.1.3 ^3.4.0 MIT
1.1.2 ^3.4.0 MIT
1.1.0 ^3.4.0 MIT
0.1.1 ^3.4.0 MIT
0.1.0 ^3.4.0 MIT
Pre-releases (1)
Version Released License
0.0.0-stage