Fast, streaming-friendly Markdown rendering for Vue 3 โ progressive Mermaid, streaming diff code blocks, and real-time previews optimized for large documents.
Looking for other frameworks?
- Vue 2.6: use
markstream-vue2(a baseline port with fewer advanced features) - React: see
packages/markstream-reactatpackages/markstream-react/README.md(first-pass port)
markstream-vue@1.0 is scoped to the Vue 3 renderer package. The stable surface is MarkdownRender, streaming content rendering, pre-parsed node rendering, the safe HTML policy, optional Mermaid / KaTeX / Monaco / D2 / Infographic integrations, virtual-scroll coordination, CSS exports, worker client subpaths, and SSR imports for Vite / Nuxt / VitePress.
Experimental surfaces are the cross-framework adapters, repository skills/prompts, low-level worker entrypoints beyond documented clients, and internal performance/debug props such as indexKey, renderAsFragment, debugPerformance, batch budget internals, and height-estimation experiments.
For the full release contract and Go / No-Go checklist, see 1.0 Release Readiness. For reproducible performance evidence, run pnpm benchmark:1.0 and use the generated 1.0 Benchmark Report.
- TL;DR Highlights
- Choose Your Path
- Try It Now
- Community & support
- Quick Start
- Common commands
- Streaming in 30 seconds
- Performance presets
- Key props & options
- Where it shines
- FAQ
- Why markstream-vue
- Roadmap
- Releases
- Showcase
- Introduction Video
- Features
- Contributing & community
- Troubleshooting
- Thanks
- Star History
- License
๐ Detailed docs, API, and advanced usage: https://markstream-vue-docs.simonhe.me/guide/
- Purpose-built for streaming Markdown (AI/chat/SSE) with zero flicker and predictable memory.
- Two render modes: virtual window for long docs, incremental batching for โtypingโ effects.
- Progressive diagrams (Mermaid) and streaming code blocks (Monaco/Shiki) that keep up with diffs.
- Works with raw Markdown strings or pre-parsed nodes, supports custom Vue components inline.
- TypeScript-first, ship-ready defaults โ import CSS and render.
| If you want to... | Start here | Then go to |
|---|---|---|
| get the first render on screen | Quick Start | Installation guide |
| integrate it into a docs site or VitePress theme | Docs Site & VitePress | Custom Tags & Advanced Components |
| build an AI chat UI or SSE stream | AI Chat & Streaming | Performance |
| replace one built-in renderer | Override Built-in Components | Renderer & Node Components |
add trusted tags such as thinking |
Custom Tags & Advanced Components | API Reference |
| debug a broken integration but do not know why yet | Troubleshooting by Symptom | Troubleshooting |
- Playground (interactive demo): https://markstream-vue.simonhe.me/
- Interactive test page (shareable links, easy reproduction): https://markstream-vue.simonhe.me/test
- Docs: https://markstream-vue-docs.simonhe.me/guide/
- Showcase: https://markstream-vue-docs.simonhe.me/guide/showcase
- 1.0 benchmark report: run
pnpm benchmark:1.0 - AI/LLM context (project map): https://markstream-vue-docs.simonhe.me/llms
- AI/LLM context (ไธญๆ): https://markstream-vue-docs.simonhe.me/llms.zh-CN
- One-click StackBlitz demo: https://stackblitz.com/github/Simon-He95/markstream-vue?file=playground/src/App.vue
- Changelog: CHANGELOG.md
- Nuxt playground:
pnpm play:nuxt - Discord: https://discord.gg/vkzdkjeRCW
If you want the AI assets without cloning the repo:
npx skills add Simon-He95/markstream-vueRecommended usage:
npx skills add Simon-He95/markstream-vueis the primary path for Codex-compatible skill discovery because it reads.agents/skillsdirectly from the GitHub repositorymarkstream-vue@1.0no longer exposes themarkstream-vueCLI or any CLIbin; repository scripts such aspnpm skills:listandpnpm prompts:listare contributor-only helpers for cloned checkouts- prompts remain in the repository under
prompts/for direct copying or future separate-package work
Other npx skills add forms also work:
# Full GitHub URL
npx skills add https://github.com/Simon-He95/markstream-vue
# Direct path to one skill in this repo
npx skills add https://github.com/Simon-He95/markstream-vue/tree/main/.agents/skills/markstream-install
# Any git URL
npx skills add git@github.com:Simon-He95/markstream-vue.git- Discussions: https://github.com/Simon-He95/markstream-vue/discussions
- Discord: https://discord.gg/vkzdkjeRCW
- Issues: please use templates and attach a repro link (https://markstream-vue.simonhe.me/test)
The test page gives you an editor + live preview plus โgenerate share linkโ that encodes the input in the URL (with a fallback to open directly or pre-fill a GitHub Issue for long payloads).
If markstream-vue helps your work, you can support ongoing maintenance with one of these QR codes.
| Alipay | WeChat Pay |
|---|---|
![]() |
![]() |
pnpm add markstream-vue
# npm install markstream-vue
# yarn add markstream-vueimport MarkdownRender from 'markstream-vue'
// main.ts
import { createApp } from 'vue'
import 'markstream-vue/index.css'
createApp({
components: { MarkdownRender },
template: '<MarkdownRender custom-id="docs" :content="doc" />',
setup() {
const doc = '# Hello from markstream-vue\\n\\nSupports **streaming** nodes.'
return { doc }
},
}).mount('#app')Import markstream-vue/index.css after your reset (e.g., use @import 'markstream-vue/index.css' layer(components); for Tailwind) so renderer styles win over utility classes. Install optional peers such as stream-monaco, shiki, stream-markdown, mermaid, and katex only when you need Monaco code blocks, Shiki highlighting, diagrams, or math.
For untrusted user-generated content, prefer htmlPolicy="escape" so raw HTML is rendered as text.
If your app intentionally scales root font size on mobile, use markstream-vue/index.px.css to avoid rem-based global scaling side effects.
Choose the renderer mode by surface:
<!-- AI chat / SSE output: steady pacing, no opacity animation flicker -->
<MarkdownRender
mode="chat"
:content="message"
:final="isDone"
smooth-streaming="auto"
:fade="false"
/>
<!-- Rich docs: larger render batches, tooltips, and fade are enabled by default -->
<MarkdownRender
mode="docs"
:content="doc"
:final="true"
/>Use mode="minimal" when you want the same lightweight defaults as chat, but prefer a neutral mode name for non-chat surfaces. Avoid combining high-frequency smooth-streaming with fade; it can turn a steady stream into repeated opacity restarts.
For the same chat message, do not switch from mode="chat" to mode="docs" only because final changed. Keep the mode stable and switch pacing/animation props (smooth-streaming, typewriter, fade) instead; docs changes the default code renderer and layout strategy.
For docs pages that do not need Monaco-backed code blocks, set :render-code-blocks-as-pre="true". If you want the rich Monaco-backed code-block UI, install stream-monaco; otherwise the renderer intentionally falls back to <pre> rendering.
Renderer CSS is scoped under an internal .markstream-vue container to minimize global style conflicts. If you render exported node components outside of MarkdownRender, wrap them in an element with class markstream-vue.
For dark theme variables, either add a .dark class on an ancestor, or pass :is-dark="true" to MarkdownRender to scope dark mode to the renderer.
Prefer the unified code-block theme prop for new integrations. When you render through MarkdownRender, pass it via code-block-props:
<MarkdownRender
:is-dark="isDark"
:code-block-props="{ theme: { light: 'vitesse-light', dark: 'vitesse-dark' } }"
:content="doc"
/>Language icons use the built-in material theme by default. For new integrations, inspect or switch icon themes with the exported helpers before app.mount(). The legacy app.use(VueRendererMarkdown, { iconTheme }) option still works in 1.x, but prefer helpers because icon configuration is process-global state.
import { getRegisteredThemes, setIconTheme } from 'markstream-vue'
console.log(getRegisteredThemes()) // ['material']
setIconTheme('material')Use registerIconTheme() if you want to add your own icon pack.
Enable heavy peers only when needed:
import { enableKatex, enableMermaid } from 'markstream-vue'
import 'markstream-vue/index.css'
import 'katex/dist/katex.min.css'
// after you install `mermaid` / `katex` peers
enableMermaid()
enableKatex()Optional: CDN-backed workers (KaTeX / Mermaid)
If you load KaTeX via CDN and want KaTeX rendering in a Web Worker (no bundler / optional peer not installed), inject a CDN-backed worker:
import { createKaTeXWorkerFromCDN, setKaTeXWorker } from 'markstream-vue'
const { worker } = createKaTeXWorkerFromCDN({
mode: 'classic',
// UMD builds used by importScripts() inside the worker
katexUrl: 'https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/katex.min.js',
mhchemUrl: 'https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/contrib/mhchem.min.js',
})
if (worker)
setKaTeXWorker(worker)If you load Mermaid via CDN and want off-main-thread parsing (used by progressive Mermaid rendering), inject a Mermaid parser worker:
import { createMermaidWorkerFromCDN, setMermaidWorker } from 'markstream-vue'
const { worker } = createMermaidWorkerFromCDN({
// Mermaid CDN builds are commonly ESM; module worker is recommended.
mode: 'module',
workerOptions: { type: 'module' },
mermaidUrl: 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs',
})
if (worker)
setMermaidWorker(worker)Nuxt quick drop-in
// plugins/markstream-vue.client.ts
import { defineNuxtPlugin } from '#app'
import MarkdownRender from 'markstream-vue'
import 'markstream-vue/index.css'
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.vueApp.component('MarkdownRender', MarkdownRender)
})Then use <MarkdownRender :content="md" /> in your pages.
pnpm devโ playground dev serverpnpm play:nuxtโ Nuxt playground devpnpm buildโ library + CSS buildpnpm build:analyzeโ build with bundle visualizer reports (bundle-visualizer.html,bundle-visualizer-tailwind.html)pnpm size:checkโ run dist + npm package size budget checks (same guard used in CI)pnpm testโ Vitest suite (pnpm test:updatefor snapshots)pnpm typecheck/pnpm lintโ type and lint checks
Render streamed Markdown (SSE/websocket) with built-in smooth pacing:
import MarkdownRender from 'markstream-vue'
import { ref } from 'vue'
const content = ref('')
const final = ref(false)
eventSource.onmessage = (event) => {
content.value += event.data
}
eventSource.addEventListener('done', () => {
final.value = true
})
// template
// <MarkdownRender
// :content="content"
// :final="final"
// :max-live-nodes="0"
// :batch-rendering="true"
// :render-batch-size="16"
// :render-batch-delay="8"
// :render-batch-budget-ms="4"
// :fade="false"
// :typewriter="true"
// />smooth-streaming is enabled by default in typewriter/incremental mode (typewriter or max-live-nodes <= 0). Disable per surface with :smooth-streaming="false" if you want raw chunk cadence.
Switch rendering style per surface:
- Virtualized window (default): steady scrolling and memory usage for long docs.
- Incremental batches: set
:max-live-nodes="0"for AI-like โtypingโ with lightweight placeholders.
Advanced: SSR / worker / streaming handoff
Pre-parse Markdown on the server or in a worker and render typed nodes on the client:
// server or worker
import { getMarkdown, parseMarkdownToStructure } from 'markstream-vue'
const md = getMarkdown()
const nodes = parseMarkdownToStructure('# Hello\n\nThis is parsed once', md)
// send `nodes` JSON to the clientWarning:
parseMarkdownToStructuredefaults tostreamParse: 'auto': compatiblemdinstances usemd.stream.parsefor non-final top-level parses and retain the latest source/token cache. Final one-shot parses use the regular parser unless you pass{ streamParse: true }; pass{ streamParse: false }to opt out. If you reuse onemdinstance for unrelated one-shot documents, pass{ final: true }or{ streamParse: false }.
const nodes = parseMarkdownToStructure(source, md, { final: true })When MarkdownRender parses its own content, it intentionally defaults parseOptions.streamParse to true so streaming parses use md.stream.parse. When final changes, the renderer invalidates the stream cache and reparses with final semantics to avoid stale loading or unclosed-token state. Pass :parse-options="{ streamParse: 'auto' }" to keep final content parses on the regular parser, or false to opt out entirely.
<!-- client -->
<MarkdownRender :nodes="nodesFromServer" />This avoids client-side parsing and keeps SSR/hydration deterministic.
- Server: parse the first Markdown batch to nodes and serialize
initialNodes(and the rawinitialMarkdownif you also stream later chunks). - Client: hydrate with the same parser options, then keep streaming:
import type { ParsedNode } from 'markstream-vue'
import { getMarkdown, parseMarkdownToStructure } from 'markstream-vue'
import { ref } from 'vue'
const nodes = ref<ParsedNode[]>(initialNodes)
const buffer = ref(initialMarkdown)
const md = getMarkdown() // match server setup
function addChunk(chunk: string) {
buffer.value += chunk
nodes.value = parseMarkdownToStructure(buffer.value, md, { final: false })
}This avoids re-parsing SSR content while letting later SSE/WebSocket chunks continue the stream.
Tip: when you know the stream has ended (the message is complete), use
parseMarkdownToStructure(buffer.value, md, { final: true })or pass:final="true"to the component. This disables mid-state (loading) parsing so trailing delimiters (like$$or an unclosed code fence) wonโt get stuck showing perpetual loading.
- Virtual window (default) โ keep
max-live-nodesat its default320to enable virtualization. Nodes render immediately and the renderer keeps a sliding window of elements mounted so long docs remain responsive without showing skeleton placeholders. - Incremental stream โ set
:max-live-nodes="0"when you want a true typewriter effect. This disables virtualization and turns on incremental batching governed bybatchRendering,initialRenderBatchSize,renderBatchSize,renderBatchDelay, andrenderBatchBudgetMs, so new content flows in small slices with lightweight placeholders.
Pick one mode per surface: virtualization for best scrollback and steady memory usage, or incremental batching for AI-style โtypingโ previews.
Tip: In chats, combine
max-live-nodes="0"with smallrenderBatchSize(e.g.,16) and a tinyrenderBatchDelay(e.g.,8ms) to keep the โtypingโ feel smooth without jumping large chunks. TunerenderBatchBudgetMsdown if you need to cap CPU per frame.
contentvsnodes: pass raw Markdown or pre-parsed nodes (fromparseMarkdownToStructure).max-live-nodes:320(default virtualization) or0(incremental batches).batchRendering: fine-tune batches withinitialRenderBatchSize,renderBatchSize,renderBatchDelay,renderBatchBudgetMs.enableMermaid/enableKatex: (re)enable heavy peers or custom loaders when needed (pairs withdisableMermaid/disableKatex).parse-options: reuse parser hooks (e.g.,preTransformTokens,requireClosingStrong) on the component.final: marks end-of-stream; disables mid-state loading parsing and forces unfinished constructs to settle.custom-html-tags: extend streaming HTML allowlist for custom tags and emit them as custom nodes forsetCustomComponents(e.g.,['thinking']). Declared custom nodes keepcontent/rawclose to the original tag payload, whilechildrenremains the Markdown-rendered form for rich text.setCustomComponents(customId?, mapping): register inline Vue components for custom tags/markers (scoped bycustom-idwhen provided).
Example: map Markdown placeholders to Vue components (scoped)
import { setCustomComponents } from 'markstream-vue'
setCustomComponents('docs', {
CALLOUT: () => import('./components/Callout.vue'),
})
// Markdown: [[CALLOUT:warning title="Heads up" body="Details here"]]Use the same custom-id on the renderer:
<MarkdownRender
:content="doc"
custom-id="docs"
/>Parse hooks example (match server + client):
<MarkdownRender
:content="doc"
:parse-options="{
requireClosingStrong: true,
preTransformTokens: (tokens) => tokens,
}"
/>- AI/chat UIs with long-form answers and Markdown tokens arriving over SSE/websocket.
- Docs, changelogs, and knowledge bases that need instant load but stay responsive as they grow.
- Streaming diffs and code review panes that benefit from Monaco live updates.
- Diagram-heavy content that should render progressively (Mermaid) without blocking.
- Embedding Vue components in Markdown-driven surfaces (callouts, widgets, CTA buttons).
- Mermaid/KaTeX not rendering? Install the peer (
mermaid/katex) and pass:enable-mermaid="true"/:enable-katex="true"or call the loader setters. If you load them via CDN script tags, the library will also pick upwindow.mermaid/window.katex. - CDN + KaTeX worker: if you don't bundle
katexbut still want off-main-thread rendering, create and inject a worker that loads KaTeX via CDN (UMD) usingcreateKaTeXWorkerFromCDN()+setKaTeXWorker(). - Bundle size: peers are optional and not bundled; import only
markstream-vue/index.cssonce; use Shiki (MarkdownCodeBlockNode) when Monaco is too heavy. Infrequent language icons are split into an async chunk and load on demand; callpreloadExtendedLanguageIcons()during app idle if you want to avoid first-hit icon fallback. - Custom UI: register components via
setCustomComponents(global or scoped), then emit markers/placeholders in Markdown and map them to Vue components.
| Needs | Typical Markdown preview | markstream-vue |
|---|---|---|
| Streaming input | Re-renders whole tree, flashes | Incremental batches with virtual windowing |
| Large code blocks | Slow re-highlight | Monaco streaming updates + Shiki option |
| Diagrams | Blocks while parsing | Progressive Mermaid with graceful fallback |
| Custom UI | Limited slots | Inline Vue components & typed nodes |
| Long docs | Memory spikes | Configurable live-node cap for steady usage |
- More โinstant startโ templates (Vite + Nuxt + Tailwind) and updated StackBlitz.
- Additional codeblock presets (diff-friendly Shiki themes, Monaco decoration helpers).
- Cookbook docs for AI/chat patterns (SSE/WebSocket, retry/resume, markdown mid-states).
- More showcase examples for embedding Vue components inside Markdown surfaces.
- Latest: Releases โ see highlights and upgrade notes.
- Full history: CHANGELOG.md
- 1.0 launch notes:
- Stable Vue 3 renderer API, SSR imports, CSS exports, Tailwind export, worker client exports, and safe HTML defaults.
markstream-vue@1.0.0,markstream-core@1.0.0, andstream-markdown-parser@1.0.0ship together.- Public benchmark report: run
pnpm benchmark:1.0or use the1.0 Benchmarkworkflow artifact. - Migration guide: Migrating to 1.0.
Build something with markstream-vue? Open a PR to add it here (include a link + 1 screenshot/GIF). Ideal fits: AI/chat UIs, streaming docs, diff/code-review panes, or Markdown-driven pages with embedded Vue components.
- FlowNote โ streaming Markdown note app demo (SSE + virtual window) โ https://markstream-vue.simonhe.me/
- Diagnostic Studio โ shareable repro links, render-mode switching, diff/thinking/stress samples, annotations, PDF export โ https://markstream-vue.simonhe.me/test
- 1.0 Showcase guide โ launch-ready demo matrix for chat, long docs, code review, diagrams, custom components, and safe HTML โ https://markstream-vue-docs.simonhe.me/guide/showcase
A short video introduces the key features and usage of markstream-vue:
Watch on Bilibili: Open in Bilibili
- โก Extreme performance: minimal re-rendering and efficient DOM updates for streaming scenarios
- ๐ Streaming-first: native support for incomplete or frequently updated tokenized Markdown
- ๐ง Monaco streaming updates: high-performance Monaco integration for smooth incremental updates of large code blocks
- ๐ช Progressive Mermaid: charts render instantly when syntax is available, and improve with later updates
- ๐งฉ Custom components: embed custom Vue components in Markdown content
- ๐ Full Markdown support: tables, formulas, emoji, checkboxes, code blocks, etc.
- ๐ Real-time updates: supports incremental content without breaking formatting
- ๐ฆ TypeScript-first: complete type definitions and IntelliSense
- ๐ Zero config: works out of the box in Vue 3 projects
- ๐จ Flexible code block rendering: choose Monaco editor (
CodeBlockNode) or lightweight Shiki highlighting (MarkdownCodeBlockNode) - ๐งฐ Parser toolkit:
stream-markdown-parsernow documents how to reuse the parser in workers/SSE streams and feed<MarkdownRender :nodes>directly, plus APIs for registering global plugins and custom math helpers.
- Read the contributor guide: CONTRIBUTING.md and follow the PR template.
- Be kind and follow the Code of Conduct.
- Issues: use templates for bugs/requests; attach a repro from https://markstream-vue.simonhe.me/test when possible.
- Questions? Start a discussion: https://github.com/Simon-He95/markstream-vue/discussions
- Chat live: Discord https://discord.gg/vkzdkjeRCW
- Looking to contribute? Start with good first issues.
- Support guide: SUPPORT.md
- PRs: follow Conventional Commits, add tests for parser/render changes, and include screenshots/GIFs for UI tweaks.
- If the project helps you, consider starring and sharing the repo โ it keeps the work sustainable.
- Security: see SECURITY.md to report vulnerabilities privately.
- Add repro links/screenshots to existing issues.
- Improve docs/examples (especially streaming + SSR/worker patterns).
- Share playground/test links that showcase performance edge cases.
Troubleshooting has moved into the docs: https://markstream-vue-docs.simonhe.me/guide/troubleshooting
If you can't find a solution there, open a GitHub issue: https://github.com/Simon-He95/markstream-vue/issues
- Reproduce in the test page and click โgenerate share linkโ: https://markstream-vue.simonhe.me/test
- Open a bug report with the link and a screenshot: https://github.com/Simon-He95/markstream-vue/issues/new?template=bug_report.yml
Thanks to all the people who have contributed to this project!
This project uses and benefits from:
Thanks to the authors and contributors of these projects!


