nbeditor
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 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.cssso 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')
renderMarkdownreturns HTML with<div class="mermaid">placeholders already in place. CalluseMarkdownRenderer().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
.docxis 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
Streaming markdown output, Useful for text streams like LLM outputs.
Streamdown-style streaming Markdown renderer for Vue 3/Nuxt 3 with Shiki, KaTeX,...
**fo-markdown-note** is a **Vue.js** component that provides a simple Markdown e...
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 |