Code Block Rendering
Overview
Code blocks can be rendered in two ways depending on which optional dependency you install:
- Enhanced surface (recommended for large or interactive code blocks): install
stream-diffsfor File and FileDiff rendering, syntax highlighting, and diff interactions.CodeBlockNodeloads the core runtime on demand after the code block has completed streaming and entered the viewport. - Fallback (no extra deps): if
stream-diffsis not installed, code blocks render as plain<pre><code>blocks with basic styling.
stream-diffs surface (recommended)
- Install:
pnpm add stream-diffs
# or
npm i stream-diffs- Boundary: the
stream-diffsroot entry is framework-agnostic. Its controllers receive anHTMLElementand plain code/diff data; it has no Vue lifecycle.stream-diffs/vueis a separate optional convenience entry and is not used bymarkstream-vue. - Behavior: this Vue adapter keeps one shared
PreCodeBlocksurface while content is streaming.renderCodeBlocksAsPreuses that exact component and the same resolved defaults. Once the block is complete and visible,CodeBlockNodemounts onestream-diffsFile or FileDiff surface and applies language highlighting. - The shared pre and enhanced surfaces use the same resolved font size, line height, font family, tab size, padding, overflow, line-number gutter, and theme background. The zero-config pre background is
vitesse-dark(#121212) whenisDarkis true andvitesse-light(#ffffff) otherwise, matching the default enhanced theme before the first highlighted frame. - The fallback and enhanced surfaces reserve a four-character minimum line-number column. This keeps the gutter stable while streamed content crosses the 10, 100, or 1000 line boundary; longer line numbers expand the column as needed.
CodeBlockShellowns the title and action bar. The innerdata-diffs-headeris disabled so File surfaces do not render a second header.- No worker plugin or extra CSS import is required for this integration. See also: /guide/code-block-runtime for runtime and preload details.
Configuration
Use the renderer-neutral codeBlockOptions prop on either MarkdownRender / NodeRenderer or a directly mounted CodeBlockNode. The same public CodeBlockOptions type is exported by all six framework adapters.
<script setup lang="ts">
import type { CodeBlockOptions } from 'markstream-vue'
const codeBlockOptions: CodeBlockOptions = {
fontSize: 13,
overflow: 'wrap',
diffStyle: 'unified',
expandUnchanged: false,
enableLineSelection: true,
}
</script>
<template>
<MarkdownRender
:content="content"
:code-block-options="codeBlockOptions"
/>
</template>Typography/layout fields (fontSize, lineHeight, fontFamily, numeric-pixel maxHeight, numeric-pixel symmetric padding, tabSize) are coordinated by Markstream so the streaming fallback and finalized surface match. Supported File/FileDiff fields include disableLineNumbers, overflow, highlighter limits, diff layout/folding, interactions, selection callbacks, annotations, onController, and workerManager. Theme, language/content, streaming state, header, mounting, reveal, and disposal stay host-owned and take precedence.
Themes are registered string names. Direct CodeBlockNode.theme accepts a string or { dark, light }, while themes is the [dark, light] pair to load. A former Monaco JSON theme object has no direct rename: use registerCustomTheme from stream-diffs/pierre, then pass its name.
See /guide/code-block-runtime for the full runtime behavior, diff interactions, and optional preload.
Theming the fallback surface
The shared PreCodeBlock fallback (shown while content streams, used by renderCodeBlocksAsPre, and retained when no enhanced runtime is installed) resolves its background from the same host-owned theme selection as the enhanced surface. The default pair is vitesse-dark / vitesse-light; custom theme names may provide matching fallback colors through --markstream-code-theme-bg and --markstream-code-theme-fg. The remaining shell tokens — --code-border, --code-header-bg, --code-action-fg, --code-line-number, etc. — continue to control shared chrome.
Language icon lazy loading
To keep the main bundle smaller, infrequent language icons are split into an async chunk:
- Common languages (JS/TS/HTML/CSS/JSON/Python/etc.) stay in the main bundle.
- Rare languages load on demand and will update icon output automatically after the async chunk resolves.
- If you prefer to avoid first-hit fallback icons, preload once during app idle:
import { preloadExtendedLanguageIcons } from 'markstream-vue'
if (typeof window !== 'undefined')
void preloadExtendedLanguageIcons()Fallback
If you don't install stream-diffs, the code block loader returns null and the renderer falls back to a simple pre/code representation. The fallback still shows line numbers and follows the --code-* theming tokens.
Fallback line numbers count logical source lines delimited by \n, \r\n, or \r. When codeBlockOptions.overflow is wrap, a long logical line may occupy multiple visual rows, but it keeps one line number and pushes the next logical line down by its wrapped height:
1 │ const short = true
2 │ const long = "one logical source line that wraps
│ onto another visual row"
3 │ return longAutomatic wrapping never creates an additional source line number or diff row. The default is overflow: 'wrap', and the same value is applied to the fallback and enhanced code surface. With overflow: 'scroll', both surfaces keep non-wrapping content and horizontal scrolling.
For unified and split diffs, the fallback and finalized surface use the same unchanged-region threshold. No newline at end of file is shown only for a source that actually lacks a final LF, uses a neutral metadata foreground/background, and occupies the same visible height in both surfaces. Added and removed line backgrounds have the same effective composited color in both surfaces; the fallback must not paint the same translucent fill in overlapping layers. When diff rows wrap, the added/removed content background, gutter marker, and line-number background fill the complete logical row; changing between wrap and scroll discards the previous measured row height immediately.
maxHeight is also a shared visibility boundary. Content that fits below it remains fully visible in both surfaces. Content that exceeds it remains reachable through the same internal scrolling behavior; the finalized diff host must not clip rows with overflow: hidden while the outer shell keeps a larger blank height.
Links & further reading
- Worker / SSR guidance: /nuxt-ssr
- Installation notes: /guide/installation
Try this — simple CodeBlock render:
<script setup lang="ts">
import type { CodeBlockNodeProps } from 'markstream-vue'
import { CodeBlockNode } from 'markstream-vue'
const node = {
type: 'code_block',
language: 'js',
code: 'console.log(42)',
raw: 'console.log(42)',
} satisfies CodeBlockNodeProps['node']
</script>
<template>
<CodeBlockNode
:node="node"
:code-block-options="{ overflow: 'wrap', disableLineNumbers: true }"
/>
</template>