Multi-framework streaming Markdown renderers for AI apps: Vue/Nuxt, React/Next.js, Svelte, and Angular, with Mermaid, KaTeX, stream-diffs code blocks, safe HTML, and low-jitter updates.
See the codemarkstream-vue is a Vue 3 / Nuxt / VitePress streaming Markdown renderer for AI chat, LLM token streams, SSE/WebSocket output, incomplete Markdown, long AI responses, Mermaid, KaTeX, and streaming code blocks.
Markstream is the renderer family for Vue, React, Octane, Svelte, Angular, and Vue 2. Use sibling packages for non-Vue frameworks; use markstream-vue when your Vue/Nuxt/VitePress UI needs stable partial Markdown states, mobile WebView rendering, safe component rendering, and progressive heavy blocks.
Vue package:
Other packages:
Markstream 2.x is stable on npm's latest tag:
pnpm add markstream-vue
Add the optional stream-diffs peer only when you need enhanced code and diff blocks. markstream-vue@1 pins the maintained 1.x line; it is also preserved on the legacy npm tag, with 1.x prereleases on legacy-next. See the 1.x to 2.0 migration guide for the code-block runtime changes.
For applications that must stay on the 1.x line across the 2.x cutover:
pnpm add markstream-vue@1
<script setup lang="ts">
import MarkdownRender from 'markstream-vue'
import 'markstream-vue/index.css'
defineProps<{
content: string
isDone: boolean
}>()
</script>
<template>
<MarkdownRender mode="chat" :content="content" :final="isDone" />
</template>
Use marked, markdown-it, or react-markdown for finished Markdown documents.
Use Markstream when the Markdown is still changing while the user is reading it.
Detailed comparisons: vue-stream-markdown, Streamdown, react-markdown, and marked / markdown-it.
Start with the framework overview if you are choosing between packages.
| Package | Framework | Install | Docs |
|---|---|---|---|
markstream-vue | Vue 3 / Nuxt / VitePress | pnpm add markstream-vue | Framework overview · Vue landing · Nuxt landing |
markstream-react | React / Next.js / Remix | pnpm add markstream-react | React landing · Next.js landing |
markstream-octane | Octane | pnpm add markstream-octane octane | Package guide · Local playground |
markstream-svelte | Svelte 5 | pnpm add markstream-svelte svelte@^5 | Svelte landing · Quick start |
markstream-angular | Angular standalone | pnpm add markstream-angular | Angular landing · Quick start |
markstream-vue2 | Vue 2.6 / 2.7 | pnpm add markstream-vue2 | Vue 2 landing · Quick start |
stream-markdown-parser | Any JS/TS app | pnpm add stream-markdown-parser | Parser guide |
markstream-core | Framework-agnostic | pnpm add markstream-core | Core package |
markstream-vuemarkstream-reactmarkstream-octanemarkstream-sveltemarkstream-angularmarkstream-vue2stream-markdown-parsermarkstream-coremarkstream-vue 2.x is stable and published on npm's latest tag. The maintained 1.x line remains available through markstream-vue@1 and the legacy tag. Version 2 removes the Monaco and stream-markdown code-block runtimes; install the optional stream-diffs peer when you need the enhanced code-block surface. See Migrating from 1.x to 2.0 before upgrading.
The stable surface includes MarkdownRender, streaming content rendering, pre-parsed node rendering, the safe HTML policy, optional Mermaid / KaTeX / D2 / Infographic integrations, enhanced code blocks, virtual-scroll coordination, CSS exports, worker client subpaths, and SSR imports for Vite / Nuxt / VitePress.
Cross-framework renderers (markstream-react, markstream-octane, markstream-svelte, markstream-angular, markstream-vue2) are available and actively developed. Check each package page for API maturity, framework support, and known limitations.
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.
📖 Framework overview, docs, API, and advanced usage: https://markstream.simonhe.me/frameworks
| If you want to... | Start here | Then go to |
|---|---|---|
| get the first render on screen | Framework overview | Quick Starts |
| 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 |
| Framework | Playground |
|---|---|
| Vue 3 | https://markstream-vue.simonhe.me/ |
| React | https://markstream-react.pages.dev/ |
| Octane | pnpm play:octane |
| Svelte | https://markstream-svelte.pages.dev/ |
| Angular | https://markstream-angular.pages.dev/ |
| Nuxt | https://markstream-nuxt.pages.dev/ |
| Vue 2 | https://markstream-vue2.pages.dev/ |
pnpm benchmark:1.0If you want the AI assets without cloning the repo:
npx skills add Simon-He95/markstream-vue
Recommended usage:
npx skills add Simon-He95/markstream-vue is the primary path for Codex-compatible skill discovery because it reads .agents/skills directly from the GitHub repositorymarkstream-migration skill when upgrading an existing Markstream 1.x application to 2.0markstream-vue@1.0 no longer exposes the markstream-vue CLI or any CLI bin; repository scripts such as pnpm skills:list and pnpm prompts:list are contributor-only helpers for cloned checkoutsprompts/ for direct copying or future separate-package workOther 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
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).
pnpm add markstream-vue
<script setup>
import MarkdownRender from 'markstream-vue'
import 'markstream-vue/index.css'
</script>
<template>
<MarkdownRender :content="content" />
</template>
pnpm add markstream-react
import MarkdownRender from 'markstream-react'
import 'markstream-react/index.css'
export function Message({ content, isDone }: { content: string, isDone: boolean }) {
return <MarkdownRender content={content} final={isDone} fade={false} />
}
For live SSE/WebSocket surfaces in Next.js, use root markstream-react inside a 'use client' component. For SSR-first or server-only Markdown, start from the Next.js guide.
pnpm add markstream-octane octane
import { NodeRenderer } from 'markstream-octane'
import 'markstream-octane/index.css'
export function Message(props: { content: string, isDone: boolean }) {
return <NodeRenderer content={props.content} final={props.isDone} />
}
Add octane/compiler/vite to Vite and configure .tsrx as shown in the package guide. The package ships precompiled client and server entries; application source still uses the normal Octane compiler.
pnpm add markstream-svelte svelte@^5
<script lang="ts">
import MarkdownRender from 'markstream-svelte'
import 'markstream-svelte/index.css'
let { content = '# Hello from markstream-svelte' }: { content?: string } = $props()
</script>
<MarkdownRender {content} />
pnpm add markstream-angular
import { Component, signal } from '@angular/core'
import { bootstrapApplication } from '@angular/platform-browser'
import { MarkstreamAngularComponent } from 'markstream-angular'
import 'markstream-angular/index.css'
@Component({
selector: 'app-root',
standalone: true,
imports: [MarkstreamAngularComponent],
template: '<markstream-angular [content]="content()" [final]="true" />',
})
class AppComponent {
readonly content = signal('# Hello from markstream-angular')
}
bootstrapApplication(AppComponent)
pnpm add markstream-vue
# npm install markstream-vue
# yarn add markstream-vue
import 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-diffs, mermaid, and katex only when you need enhanced code blocks and diffs, 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. In Vue 3 (including Nuxt), smooth-streaming controls output pacing and fade controls opacity; they can be enabled together. mode="chat" keeps fade=false as a lightweight default. Add fade when gradual text reveal is desired; keep it off when animation cost matters more.
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 layout strategy.
For surfaces that do not need enhanced code blocks, set :render-code-blocks-as-pre="true". If you want the rich CodeBlockNode UI and File/Diff rendering, install stream-diffs; otherwise the renderer intentionally falls back to <pre> rendering. To own ordinary fenced-code rendering, register a scoped code_block with setCustomComponents.
stream-diffs is a framework-agnostic DOM runtime. CodeBlockNode owns the Vue-side decision of when to replace the streaming <pre> with its finalized File or FileDiff surface.
Use the top-level code-block-options prop to configure the built-in surface; direct CodeBlockNode usage accepts the same codeBlockOptions object. CodeBlockOptions is shared across all six framework adapters. It covers host-managed typography/layout (fontSize, lineHeight, fontFamily, numeric-pixel maxHeight, numeric-pixel symmetric padding, tabSize) plus supported File/FileDiff, interaction, annotation, and callback fields. Theme, code/language, stream state, header, mounting, reveal, and disposal remain host-owned.
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"
/>
Theme values are registered names: direct CodeBlockNode.theme accepts a fixed string or { dark, light }, and themes is the [dark, light] pair to load. A former Monaco JSON theme object is not renamed directly; call registerCustomTheme from stream-diffs/pierre, then pass the registered name.
code-block-props forwards user-facing code block props only. Structural renderer keys such as node, key, ref, ctx, renderNode, indexKey, __proto__, prototype, and constructor are ignored.
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()
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)
// 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:update for snapshots)pnpm typecheck / pnpm lint — type and lint checksRender 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:
:max-live-nodes="0" for AI-like “typing” with lightweight placeholders.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 client
Warning:
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.
initialNodes (and the raw initialMarkdown if you also stream later chunks).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.
max-live-nodes at its default 220 to enable virtualization. Nodes render immediately and the renderer keeps a sliding window of elements mounted so long docs remain responsive without showing skeleton placeholders.:max-live-nodes="0" when you want a true typewriter effect. This disables virtualization and turns on incremental batching governed by batchRendering, initialRenderBatchSize, renderBatchSize, renderBatchDelay, and renderBatchBudgetMs, 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.
content vs nodes: pass raw Markdown or pre-parsed nodes (from parseMarkdownToStructure).max-live-nodes: 220 (default virtualization) or 0 (incremental batches).batchRendering: fine-tune batches with initialRenderBatchSize, renderBatchSize, renderBatchDelay, renderBatchBudgetMs.enableMermaid / enableKatex: (re)enable heavy peers or custom loaders when needed (pairs with disableMermaid / 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 for setCustomComponents (e.g., ['thinking']).
Declared custom nodes keep content/raw close to the original tag payload, while children remains the Markdown-rendered form for rich text.setCustomComponents(customId?, mapping): register inline Vue components for custom tags/markers (scoped by custom-id when 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,
}"
/>
If markstream-vue helps your work, you can support ongoing maintenance via GitHub Sponsors or one of these QR codes.
| Alipay | WeChat Pay |
|---|---|
![]() | ![]() |
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 up window.mermaid / window.katex.katex but still want off-main-thread rendering, create and inject a worker that loads KaTeX via CDN (UMD) using createKaTeXWorkerFromCDN() + setKaTeXWorker().markstream-vue/index.css once; enhanced code blocks load the stream-diffs runtime on demand only when it is installed. Infrequent language icons are split into an async chunk and load on demand; call preloadExtendedLanguageIcons() during app idle if you want to avoid first-hit icon fallback.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 | stream-diffs File/Diff surface |
| 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 |
markstream-vue (2.x on latest) with stream-diffs; the 1.x line stays available through @1 / the legacy tag.stream-markdown runtimes and Monaco-named APIs are removed; supported code-block options move to codeBlockOptions.markstream-vue@1.0.0, markstream-core@1.0.0, and stream-markdown-parser@1.0.0 ship together.pnpm benchmark:1.0 or use the 1.0 Benchmark workflow artifact.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.
A short video introduces the key features and usage of markstream-vue:
Watch on Bilibili: Open in Bilibili
stream-diffs File/Diff surfaces with syntax highlighting and diff interactionsstream-diffs File/Diff surface (CodeBlockNode) or plain <pre> fallback without the peerstream-markdown-parser now 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.Troubleshooting has moved into the docs: https://markstream.simonhe.me/guide/troubleshooting
If you can't find a solution there, open a GitHub issue: https://github.com/Simon-He95/markstream-vue/issues
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!
(top 24 of 39)
39,889 followers · starred Dec 2025
4,816 followers · starred Nov 2025
5,984 followers · starred Nov 2025
299 followers · starred Jul 2026
Vue
46.7%
JavaScript
28.7%
TypeScript
19.3%
Svelte
4.7%
Multi-framework streaming Markdown renderers for AI apps: Vue/Nuxt, React/Next.js, Svelte, and Angular, with Mermaid, KaTeX, stream-diffs code blocks, safe HTML, and low-jitter updates.
See the codemarkstream-vue is a Vue 3 / Nuxt / VitePress streaming Markdown renderer for AI chat, LLM token streams, SSE/WebSocket output, incomplete Markdown, long AI responses, Mermaid, KaTeX, and streaming code blocks.
Markstream is the renderer family for Vue, React, Octane, Svelte, Angular, and Vue 2. Use sibling packages for non-Vue frameworks; use markstream-vue when your Vue/Nuxt/VitePress UI needs stable partial Markdown states, mobile WebView rendering, safe component rendering, and progressive heavy blocks.
Vue package:
Other packages:
Markstream 2.x is stable on npm's latest tag:
pnpm add markstream-vue
Add the optional stream-diffs peer only when you need enhanced code and diff blocks. markstream-vue@1 pins the maintained 1.x line; it is also preserved on the legacy npm tag, with 1.x prereleases on legacy-next. See the 1.x to 2.0 migration guide for the code-block runtime changes.
For applications that must stay on the 1.x line across the 2.x cutover:
pnpm add markstream-vue@1
<script setup lang="ts">
import MarkdownRender from 'markstream-vue'
import 'markstream-vue/index.css'
defineProps<{
content: string
isDone: boolean
}>()
</script>
<template>
<MarkdownRender mode="chat" :content="content" :final="isDone" />
</template>
Use marked, markdown-it, or react-markdown for finished Markdown documents.
Use Markstream when the Markdown is still changing while the user is reading it.
Detailed comparisons: vue-stream-markdown, Streamdown, react-markdown, and marked / markdown-it.
Start with the framework overview if you are choosing between packages.
| Package | Framework | Install | Docs |
|---|---|---|---|
markstream-vue | Vue 3 / Nuxt / VitePress | pnpm add markstream-vue | Framework overview · Vue landing · Nuxt landing |
markstream-react | React / Next.js / Remix | pnpm add markstream-react | React landing · Next.js landing |
markstream-octane | Octane | pnpm add markstream-octane octane | Package guide · Local playground |
markstream-svelte | Svelte 5 | pnpm add markstream-svelte svelte@^5 | Svelte landing · Quick start |
markstream-angular | Angular standalone | pnpm add markstream-angular | Angular landing · Quick start |
markstream-vue2 | Vue 2.6 / 2.7 | pnpm add markstream-vue2 | Vue 2 landing · Quick start |
stream-markdown-parser | Any JS/TS app | pnpm add stream-markdown-parser | Parser guide |
markstream-core | Framework-agnostic | pnpm add markstream-core | Core package |
markstream-vuemarkstream-reactmarkstream-octanemarkstream-sveltemarkstream-angularmarkstream-vue2stream-markdown-parsermarkstream-coremarkstream-vue 2.x is stable and published on npm's latest tag. The maintained 1.x line remains available through markstream-vue@1 and the legacy tag. Version 2 removes the Monaco and stream-markdown code-block runtimes; install the optional stream-diffs peer when you need the enhanced code-block surface. See Migrating from 1.x to 2.0 before upgrading.
The stable surface includes MarkdownRender, streaming content rendering, pre-parsed node rendering, the safe HTML policy, optional Mermaid / KaTeX / D2 / Infographic integrations, enhanced code blocks, virtual-scroll coordination, CSS exports, worker client subpaths, and SSR imports for Vite / Nuxt / VitePress.
Cross-framework renderers (markstream-react, markstream-octane, markstream-svelte, markstream-angular, markstream-vue2) are available and actively developed. Check each package page for API maturity, framework support, and known limitations.
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.
📖 Framework overview, docs, API, and advanced usage: https://markstream.simonhe.me/frameworks
| If you want to... | Start here | Then go to |
|---|---|---|
| get the first render on screen | Framework overview | Quick Starts |
| 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 |
| Framework | Playground |
|---|---|
| Vue 3 | https://markstream-vue.simonhe.me/ |
| React | https://markstream-react.pages.dev/ |
| Octane | pnpm play:octane |
| Svelte | https://markstream-svelte.pages.dev/ |
| Angular | https://markstream-angular.pages.dev/ |
| Nuxt | https://markstream-nuxt.pages.dev/ |
| Vue 2 | https://markstream-vue2.pages.dev/ |
pnpm benchmark:1.0If you want the AI assets without cloning the repo:
npx skills add Simon-He95/markstream-vue
Recommended usage:
npx skills add Simon-He95/markstream-vue is the primary path for Codex-compatible skill discovery because it reads .agents/skills directly from the GitHub repositorymarkstream-migration skill when upgrading an existing Markstream 1.x application to 2.0markstream-vue@1.0 no longer exposes the markstream-vue CLI or any CLI bin; repository scripts such as pnpm skills:list and pnpm prompts:list are contributor-only helpers for cloned checkoutsprompts/ for direct copying or future separate-package workOther 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
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).
pnpm add markstream-vue
<script setup>
import MarkdownRender from 'markstream-vue'
import 'markstream-vue/index.css'
</script>
<template>
<MarkdownRender :content="content" />
</template>
pnpm add markstream-react
import MarkdownRender from 'markstream-react'
import 'markstream-react/index.css'
export function Message({ content, isDone }: { content: string, isDone: boolean }) {
return <MarkdownRender content={content} final={isDone} fade={false} />
}
For live SSE/WebSocket surfaces in Next.js, use root markstream-react inside a 'use client' component. For SSR-first or server-only Markdown, start from the Next.js guide.
pnpm add markstream-octane octane
import { NodeRenderer } from 'markstream-octane'
import 'markstream-octane/index.css'
export function Message(props: { content: string, isDone: boolean }) {
return <NodeRenderer content={props.content} final={props.isDone} />
}
Add octane/compiler/vite to Vite and configure .tsrx as shown in the package guide. The package ships precompiled client and server entries; application source still uses the normal Octane compiler.
pnpm add markstream-svelte svelte@^5
<script lang="ts">
import MarkdownRender from 'markstream-svelte'
import 'markstream-svelte/index.css'
let { content = '# Hello from markstream-svelte' }: { content?: string } = $props()
</script>
<MarkdownRender {content} />
pnpm add markstream-angular
import { Component, signal } from '@angular/core'
import { bootstrapApplication } from '@angular/platform-browser'
import { MarkstreamAngularComponent } from 'markstream-angular'
import 'markstream-angular/index.css'
@Component({
selector: 'app-root',
standalone: true,
imports: [MarkstreamAngularComponent],
template: '<markstream-angular [content]="content()" [final]="true" />',
})
class AppComponent {
readonly content = signal('# Hello from markstream-angular')
}
bootstrapApplication(AppComponent)
pnpm add markstream-vue
# npm install markstream-vue
# yarn add markstream-vue
import 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-diffs, mermaid, and katex only when you need enhanced code blocks and diffs, 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. In Vue 3 (including Nuxt), smooth-streaming controls output pacing and fade controls opacity; they can be enabled together. mode="chat" keeps fade=false as a lightweight default. Add fade when gradual text reveal is desired; keep it off when animation cost matters more.
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 layout strategy.
For surfaces that do not need enhanced code blocks, set :render-code-blocks-as-pre="true". If you want the rich CodeBlockNode UI and File/Diff rendering, install stream-diffs; otherwise the renderer intentionally falls back to <pre> rendering. To own ordinary fenced-code rendering, register a scoped code_block with setCustomComponents.
stream-diffs is a framework-agnostic DOM runtime. CodeBlockNode owns the Vue-side decision of when to replace the streaming <pre> with its finalized File or FileDiff surface.
Use the top-level code-block-options prop to configure the built-in surface; direct CodeBlockNode usage accepts the same codeBlockOptions object. CodeBlockOptions is shared across all six framework adapters. It covers host-managed typography/layout (fontSize, lineHeight, fontFamily, numeric-pixel maxHeight, numeric-pixel symmetric padding, tabSize) plus supported File/FileDiff, interaction, annotation, and callback fields. Theme, code/language, stream state, header, mounting, reveal, and disposal remain host-owned.
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"
/>
Theme values are registered names: direct CodeBlockNode.theme accepts a fixed string or { dark, light }, and themes is the [dark, light] pair to load. A former Monaco JSON theme object is not renamed directly; call registerCustomTheme from stream-diffs/pierre, then pass the registered name.
code-block-props forwards user-facing code block props only. Structural renderer keys such as node, key, ref, ctx, renderNode, indexKey, __proto__, prototype, and constructor are ignored.
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()
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)
// 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:update for snapshots)pnpm typecheck / pnpm lint — type and lint checksRender 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:
:max-live-nodes="0" for AI-like “typing” with lightweight placeholders.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 client
Warning:
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.
initialNodes (and the raw initialMarkdown if you also stream later chunks).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.
max-live-nodes at its default 220 to enable virtualization. Nodes render immediately and the renderer keeps a sliding window of elements mounted so long docs remain responsive without showing skeleton placeholders.:max-live-nodes="0" when you want a true typewriter effect. This disables virtualization and turns on incremental batching governed by batchRendering, initialRenderBatchSize, renderBatchSize, renderBatchDelay, and renderBatchBudgetMs, 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.
content vs nodes: pass raw Markdown or pre-parsed nodes (from parseMarkdownToStructure).max-live-nodes: 220 (default virtualization) or 0 (incremental batches).batchRendering: fine-tune batches with initialRenderBatchSize, renderBatchSize, renderBatchDelay, renderBatchBudgetMs.enableMermaid / enableKatex: (re)enable heavy peers or custom loaders when needed (pairs with disableMermaid / 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 for setCustomComponents (e.g., ['thinking']).
Declared custom nodes keep content/raw close to the original tag payload, while children remains the Markdown-rendered form for rich text.setCustomComponents(customId?, mapping): register inline Vue components for custom tags/markers (scoped by custom-id when 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,
}"
/>
If markstream-vue helps your work, you can support ongoing maintenance via GitHub Sponsors or one of these QR codes.
| Alipay | WeChat Pay |
|---|---|
![]() | ![]() |
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 up window.mermaid / window.katex.katex but still want off-main-thread rendering, create and inject a worker that loads KaTeX via CDN (UMD) using createKaTeXWorkerFromCDN() + setKaTeXWorker().markstream-vue/index.css once; enhanced code blocks load the stream-diffs runtime on demand only when it is installed. Infrequent language icons are split into an async chunk and load on demand; call preloadExtendedLanguageIcons() during app idle if you want to avoid first-hit icon fallback.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 | stream-diffs File/Diff surface |
| 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 |
markstream-vue (2.x on latest) with stream-diffs; the 1.x line stays available through @1 / the legacy tag.stream-markdown runtimes and Monaco-named APIs are removed; supported code-block options move to codeBlockOptions.markstream-vue@1.0.0, markstream-core@1.0.0, and stream-markdown-parser@1.0.0 ship together.pnpm benchmark:1.0 or use the 1.0 Benchmark workflow artifact.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.
A short video introduces the key features and usage of markstream-vue:
Watch on Bilibili: Open in Bilibili
stream-diffs File/Diff surfaces with syntax highlighting and diff interactionsstream-diffs File/Diff surface (CodeBlockNode) or plain <pre> fallback without the peerstream-markdown-parser now 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.Troubleshooting has moved into the docs: https://markstream.simonhe.me/guide/troubleshooting
If you can't find a solution there, open a GitHub issue: https://github.com/Simon-He95/markstream-vue/issues
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!
(top 24 of 39)
39,889 followers · starred Dec 2025
4,816 followers · starred Nov 2025
5,984 followers · starred Nov 2025
299 followers · starred Jul 2026
Vue
46.7%
JavaScript
28.7%
TypeScript
19.3%
Svelte
4.7%