Quick reference
| Component | Best for | Key props/events | Extra CSS / peers | Troubleshooting hooks |
|---|---|---|---|---|
MarkdownRender | Rendering full AST trees (default export) | Props: content / nodes, custom-id, final, parse-options, custom-html-tags, is-dark; events: copy, handleArtifactClick, click, mouseover, mouseout | Import markstream-vue/index.css inside a reset-aware layer (CSS is scoped under an internal .markstream-vue container) | Use setCustomComponents(customId, mapping) + custom-id to scope overrides; see CSS checklist |
CodeBlockNode | Monaco-powered code blocks, streaming diffs | node, monacoOptions, stream, loading; events: copy, previewCode; slots header-left / header-right | Install stream-monaco (peer) + bundle Monaco workers | Blank editor ⇒ check worker bundling + SSR guards |
MarkdownCodeBlockNode | Lightweight highlighting via shiki | node, stream, loading; slots header-left / header-right | Requires shiki + stream-markdown | Use for SSR-friendly or low-bundle scenarios |
MermaidBlockNode | Progressive Mermaid diagrams | node, isDark, isStrict, maxHeight; emits copy, export, openModal, toggleMode | Peer mermaid ≥ 11; no extra CSS required | For async errors see /guide/mermaid |
MathBlockNode / MathInlineNode | KaTeX rendering | node | Install katex and import katex/dist/katex.min.css | SSR requires client-only in Nuxt |
ImageNode | Custom previews/lightboxes | Props: fallback-src, show-caption, lazy, svg-min-height, use-placeholder; emits click, load, error | None, but respects global CSS | Wrap in a custom component + setCustomComponents to intercept events |
LinkNode | Animated underline, tooltips | color, underlineHeight, showTooltip | No extra CSS | Browser defaults can override a styles; import reset |
VmrContainerNode | Custom ::: containers | node (name, attrs, loading, children) | Minimal base CSS; override via setCustomComponents | JSON attrs are normalized onto node.attrs (keys without data-); invalid/partial JSON becomes attrs.attrs; args after name stored in attrs.args |
TypeScript exports
markstream-vue exports renderer and component prop interfaces:
import type {
CodeBlockNodeProps,
InfographicBlockNodeProps,
MermaidBlockNodeProps,
NodeRendererProps,
PreCodeNodeProps,
} from 'markstream-vue'
import type { CodeBlockNode } from 'stream-markdown-parser'Notes:
NodeRendererPropsmatches<MarkdownRender>props.CodeBlockNodeProps,MermaidBlockNodeProps,InfographicBlockNodeProps, andPreCodeNodePropsall useCodeBlockNodefornode(uselanguage: 'mermaid'/language: 'infographic'to route specialized renderers).
MarkdownRender
Main entry point that takes Markdown AST content (string or parsed structure) and renders with built-in node components.
Quick reference
- Best for: full markdown documents in Vite, Nuxt, VitePress.
- Key props:
content/nodes,custom-id,final,parse-options,custom-html-tags| - CSS: include a reset (
modern-css-reset,@unocss/reset, or@tailwind base) beforemarkstream-vue/index.css. Wrap import with@layer componentswhen using Tailwind/UnoCSS.
CSS scoping
markstream-vue scopes its packaged CSS under an internal .markstream-vue container to reduce global style conflicts.
- If you use
MarkdownRender, you normally don't need to do anything—it's already rendered inside that container. - If you render node components standalone (e.g.,
CodeBlockNode,MathBlockNode), wrap them with<div class="markstream-vue">...</div>so the library styles and variables apply.
Usage ladder
<script setup lang="ts">
import MarkdownRender from 'markstream-vue'
const md = '# Hello docs\n\nUse `custom-id` to scope styles.'
</script>
<template>
<MarkdownRender custom-id="docs" :content="md" />
</template>// Register custom node renderers
import { setCustomComponents } from 'markstream-vue'
import CustomImageNode from './CustomImageNode.vue'
setCustomComponents('docs', {
image: CustomImageNode,
})/* styles/main.css */
@import 'modern-css-reset';
@tailwind base;
@layer components {
@import 'markstream-vue/index.css';
}
[data-custom-id='docs'] .prose {
max-width: 720px;
}Performance knobs
- Batching —
batchRendering,initialRenderBatchSize,renderBatchSize,renderBatchDelay, andrenderBatchBudgetMsdefine how many nodes transition from placeholders to full components per frame. This incremental mode runs only when virtualization is disabled (:max-live-nodes="0"); with virtualization on, the renderer favours instant paint plus DOM windowing over skeleton placeholders. - Deferred nodes — keep
deferNodesUntilVisible+viewportPriorityenabled to let heavy blocks (Mermaid, Monaco, KaTeX) yield until they approach the viewport. Disable only when you explicitly want every node to render eagerly. - Virtualization window —
maxLiveNodescaps how many fully rendered nodes stay mounted;liveNodeBuffercontrols overscan to avoid pop-in. Tuning these lets long docs stay responsive without sacrificing scrollback. See Performance tips for sample values. - Code block fallbacks —
renderCodeBlocksAsPre+codeBlockStreamlet you fall back to lightweight<pre><code>blocks for non‑Mermaid/Infographic code blocks or pause Monaco streaming when throughput takes priority over tooling.
Combine these props with custom-id scoped styles and global parser options (setDefaultMathOptions, custom MarkdownIt plugins) to match the latency and UX expectations of your app.
Common pitfalls
- Blank styles: missing reset or incorrect layer ordering → use the CSS checklist.
- Conflicting utility classes: add
custom-idand scope overrides to[data-custom-id="..."]. - SSR errors: wrap in
<ClientOnly>(Nuxt) or guard withonMountedwhen using browser-only peers.
CodeBlockNode
Feature-rich renderer that streams Monaco tokens, supports diff markers, and header slots (
header-left,header-right).
Quick reference
- Best for: interactive editor-like blocks in docs/playgrounds.
- Peers:
stream-monaco(core), Monaco worker bundling via Vite, optional@shikijs/monacofor highlighting. - CSS: none (no extra import required).
Usage
<script setup lang="ts">
import { CodeBlockNode } from 'markstream-vue'
const node = {
type: 'code_block',
language: 'ts',
code: 'const a = 1',
raw: 'const a = 1',
}
</script>
<template>
<div class="markstream-vue">
<CodeBlockNode :node="node" :monaco-options="{ fontSize: 14 }" />
</div>
</template><!-- Advanced: custom header controls -->
<template>
<CodeBlockNode
custom-id="docs"
:node="node"
:show-copy-button="false"
>
<template #header-right>
<span class="tag">
Custom
</span>
</template>
</CodeBlockNode>
</template>HTML/SVG preview dialog
- When
node.languageishtmlorsvg(andisShowPreviewstaystrue), the toolbar exposes a Preview button. - Without a
@preview-codelistener, the built-in preview dialog is available for HTML only. - Attach
@preview-codeto handle HTML and SVG yourself. The emitted payload contains{ node, artifactType, artifactTitle, id }. Returning a listener automatically disables the built-in HTML preview overlay.
<script setup lang="ts">
import { ref } from 'vue'
const preview = ref(null)
function handlePreview(artifact) {
preview.value = artifact
}
function closePreview() {
preview.value = null
}
</script>
<template>
<CodeBlockNode
:node="node"
show-preview-button
@preview-code="handlePreview"
/>
<dialog v-if="preview" class="my-preview" open>
<header>
<strong>{{ preview.artifactTitle }}</strong>
<button type="button" @click="closePreview">
Close
</button>
</header>
<iframe
v-if="preview.artifactType === 'text/html'"
:srcdoc="preview.node.code"
sandbox="allow-scripts allow-same-origin"
/>
<div v-else v-html="preview.node.code" />
</dialog>
</template>Tip: hide the toolbar control entirely with
:show-preview-button="false". To disable previews globally, pass:code-block-props="{ isShowPreview: false }"toMarkdownRender.
Common pitfalls
- Editor invisible: worker registration missing or blocked by SSR.
- Tailwind overriding fonts: wrap imports in
@layer components. - SSR: Monaco requires browser APIs; use lazy mounts (
client-only) or guard withonMounted.
MarkdownCodeBlockNode
Lightweight code blocks using Shiki instead of Monaco — perfect for SSR/static docs or when bundle size matters.
Quick reference
- Peers:
shiki+stream-markdown. - Props: similar to
CodeBlockNode(streaming + header controls); lazy-loadsstream-markdownfor Shiki rendering. - Emits:
copy(payload: copied text),previewCode(payload:{ type, content, title }). - When to choose it: VitePress, Nuxt content sites, or anywhere Monaco would be overkill.
Usage
<script setup lang="ts">
import { MarkdownCodeBlockNode } from 'markstream-vue'
const node = {
type: 'code_block',
language: 'vue',
code: '<template><p>Hello</p></template>',
raw: '<template><p>Hello</p></template>',
}
</script>
<template>
<MarkdownCodeBlockNode :node="node" />
</template>Troubleshooting:
- Ensure
shikiis installed and properly bundled; otherwise the component falls back to plain<pre><code>. - Wrap CSS imports just like the main renderer to avoid Tailwind/Uno overrides.
MermaidBlockNode
Renders Mermaid diagrams progressively, streaming updates as soon as
mermaidparses the graph.
Quick reference
- Peer:
mermaid≥ 11 (tree-shakable ESM build recommended). - CSS: no extra Mermaid CSS import is required; keep
markstream-vue/index.cssafter your reset. - Props:
node,isDark,isStrict,maxHeight, timeouts, header/button toggles,enableWheelZoom. - Emits:
copy,export,openModal,toggleMode(callev.preventDefault()to stop the default action).
Usage
import { MermaidBlockNode } from 'markstream-vue'<script setup lang="ts">
function onExport(ev: any) {
// `ev.svgString` is available when the export button is clicked.
console.log(ev.svgString)
}
</script>
<MermaidBlockNode
:node="node"
:is-strict="true"
@export="onExport"
/>Troubleshooting:
- Async errors usually stem from missing CSS or unsupported syntax. Check browser console for Mermaid logs.
- When diagrams come from untrusted sources (user/LLM), enable
isStrictto sanitize the SVG and disable HTML labels—this closes holes wherejavascript:URLs or inline handlers could slip into the render. - When diagrams are blank in SSR, guard rendering with
onMountedor<ClientOnly>and ensure Mermaid is initialized on the client.
MathBlockNode / MathInlineNode
KaTeX-powered math display for block and inline formulas.
Quick reference
- Peer:
katex. - CSS:
import 'katex/dist/katex.min.css'. - Props:
node.
Usage
import 'katex/dist/katex.min.css'<MathBlockNode :node="node" />
<MathInlineNode :node="inlineNode" />Troubleshooting:
- Missing CSS → blank formulas or fallback text.
- Nuxt SSR needs
<ClientOnly>orclient:onlysince math rendering is client-only. - To override styling, scope selectors using
[data-custom-id]rather than editing KaTeX globals directly.
ImageNode — Custom preview handling
ImageNode emits click, load, error so you can build lightboxes or lazy loading wrappers.
<template>
<ImageNode :node="node" @click="([_ev, src]) => open(src)" />
</template>import { setCustomComponents } from 'markstream-vue'
import CustomImageNode from './ImagePreview.vue'
setCustomComponents('docs', { image: CustomImageNode })Common issues:
- Missing reset causes browser default borders—import a reset before
index.css. - Tailwind
imgutilities overriding widths—scope your overrides within[data-custom-id].
LinkNode: underline animation & color customization
LinkNode (internal anchor renderer) exposes runtime props (color, underlineHeight, showTooltip, etc.) so you can change the underline animation without CSS hacks.
<LinkNode
:node="node"
color="#e11d48"
:underline-height="3"
underline-bottom="-4px"
:animation-duration="1.2"
:show-tooltip="false"
/>Notes:
- Underline uses
currentColor; override via CSS if you need a different color. showTooltiptoggles the singleton tooltip vs native browsertitle.- Browser default anchor styles may conflict; follow the reset guidance above.
HtmlInlineNode — streaming inline HTML
HtmlInlineNode renders html_inline nodes produced by the parser (inline HTML like <span>...</span>).
Streaming behavior:
- If the node is a true mid‑state (
loading === trueandautoClosed !== true), the component renders the literal text to avoid flashing incomplete tags. - If the node is auto‑closed mid‑state (
autoClosed === true), the parser has appended a closing tag for stability. The component renders HTML viainnerHTMLbut keepsloading=trueso your app can still treat it as incomplete input. - Once the real closing tag arrives, the parser clears
loadingandautoClosedand the node renders as normal HTML.
VmrContainerNode — custom ::: containers
VmrContainerNode renders custom ::: containers with support for nested markdown content.
Quick reference
- Best for: Custom container blocks like
::: viewcode:topo-test-001 {"devId":"..."}. - Rendering: Recursively renders child nodes (paragraphs, lists, code blocks, etc.).
- CSS: Minimal base styles; override via
setCustomComponents.
Supported child nodes
The component supports the following block-level nodes inside containers:
- Inline nodes (inside paragraphs): text, strong, emphasis, link, image, inline_code, etc.
- Block nodes: paragraph, heading, list, blockquote, code_block, math_block, table
Unknown node types fall back to FallbackComponent, which displays the node type and raw content for debugging.
Syntax
::: container-name {"key":"value"}
Content here...
:::The parser extracts:
name— the container name (e.g.,viewcode:topo-test-001)attrs— parsed attributes (keys normalized fromdata-*to plain keys)children— child nodes (parsed markdown content)raw— the original raw markdown string
Node type definition
interface VmrContainerNode {
type: 'vmr_container'
name: string // Container name from ::: name
attrs?: Record<string, unknown> // Parsed attributes (values may be strings, numbers, or booleans)
loading?: boolean // Streaming mid-state: true when container is not closed
children: ParsedNode[] // Child nodes
raw: string // Raw markdown source
}Streaming behavior
When rendering containers in a streaming context (e.g., LLM output), the parser handles incomplete JSON attributes gracefully:
Loading state: When a
:::container is opened but not yet closed,loadingis set totrue. This allows your component to show an intermediate state (like a skeleton loader) while content is streaming.Attribute handling:
- Args right after the container name are stored as
attrs.args(string). - JSON attributes are normalized onto
attrs(keys without thedata-prefix), e.g.{"devId":"abc"}→attrs.devId = "abc". - If JSON parsing fails (invalid or partial JSON), the raw string is preserved in
attrs.attrs.
- Args right after the container name are stored as
Example streaming progression:
# Initially (mid-state)
::: viewcode:stream {"incomplete
# → attrs.attrs = '{"incomplete', loading = true
# After more content
::: viewcode:stream {"devId":"abc"}
# → attrs.devId = "abc", loading = false (if closing ::: present)Default rendering
The default component recursively renders all child nodes:
<!-- Default VmrContainerNode output -->
<div class="vmr-container vmr-container-container-name" v-bind="node.attrs">
<!-- Child nodes rendered here (paragraphs, lists, code blocks, etc.) -->
</div>Example content inside containers
::: info
This is a **bold** paragraph with [links](https://example.com).
## Heading inside container
- List item 1
- List item 2
```js
console.log('code blocks work too'):::
### Custom override
To customize rendering, register your component using `setCustomComponents`:
```vue
<script setup lang="ts">
import { setCustomComponents } from 'markstream-vue'
import MyViewCode from './MyViewCode.vue'
setCustomComponents('docs', {
vmr_container: MyViewCode,
})
</script>
<template>
<MarkdownRender custom-id="docs" :content="markdown" />
</template>Example: ViewCode component
Here's a complete example that renders a custom viewcode:* container:
<!-- components/ViewCodeContainer.vue -->
<script setup lang="ts">
import MarkdownRender from 'markstream-vue'
import { computed } from 'vue'
interface Props {
node: {
type: 'vmr_container'
name: string
attrs?: Record<string, unknown>
children: any[]
raw: string
}
indexKey?: number | string
customId?: string
}
const props = defineProps<Props>()
// Extract devId from attrs
const devId = computed(() => String(props.node.attrs?.devId ?? ''))
const args = computed(() => String(props.node.attrs?.args ?? ''))
// Check if this is a viewcode container
const isViewCode = computed(() => props.node.name.startsWith('viewcode:'))
</script>
<template>
<!-- Custom rendering for viewcode containers -->
<div v-if="isViewCode" class="viewcode-wrapper">
<div class="viewcode-header">
<span class="viewcode-title">{{ node.name }}</span>
<span class="viewcode-dev-id">{{ devId || args }}</span>
</div>
<div class="viewcode-content">
<MarkdownRender
:nodes="node.children"
:custom-id="customId"
:index-key="`${indexKey}-viewcode`"
/>
</div>
</div>
<!-- Fallback rendering for other containers -->
<div v-else class="vmr-container" :class="`vmr-container-${node.name}`">
<MarkdownRender
:nodes="node.children"
:custom-id="customId"
:index-key="`${indexKey}-fallback`"
/>
</div>
</template>
<style scoped>
.viewcode-wrapper {
border: 1px solid #eaecef;
border-radius: 8px;
overflow: hidden;
margin: 1rem 0;
}
.viewcode-header {
background: #f8f8f8;
padding: 0.5rem 1rem;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #eaecef;
}
.viewcode-title {
font-weight: 600;
color: #333;
}
.viewcode-dev-id {
font-family: monospace;
font-size: 0.875rem;
color: #666;
}
.viewcode-content {
padding: 1rem;
}
</style>Example: Conditional rendering by name
You can also render different components based on the container name:
<script setup lang="ts">
import { setCustomComponents } from 'markstream-vue'
import { h } from 'vue'
import AlertContainer from './AlertContainer.vue'
import ChartContainer from './ChartContainer.vue'
import GenericContainer from './GenericContainer.vue'
// Mapping of container names to components
const containerMap = {
chart: ChartContainer,
alert: AlertContainer,
}
setCustomComponents('docs', {
vmr_container: (node) => {
// Select component based on container name
const Component = containerMap[node.name as keyof typeof containerMap]
|| GenericContainer
return h(Component, { node })
},
})
</script>Troubleshooting
- Raw text visible: You're seeing the default renderer. Register a custom component via
setCustomComponents. - Attrs undefined: This is normal when you didn't pass any args/JSON. Invalid/partial JSON falls back to
attrs.attrswith the raw string. - Component not receiving props: Make sure your component accepts the
nodeprop with the correct type. - Streaming with incomplete attrs: In streaming scenarios you may temporarily see
attrs.attrscontaining partial JSON like{"incompleteuntil the full syntax arrives. Checknode.loadingto detect this mid-state.
Utility helpers
getMarkdown()— configuredmarkdown-it-tsinstance with the parser plugins this package expects.parseMarkdownToStructure(content, md)— convert Markdown strings into the AST consumed byMarkdownRender.setCustomComponents(id?, mapping)— swap any node renderer for a specificcustom-id.