Stream Markdown
A Markdown renderer built for streaming output, with a tail cursor and per-language fenced-block renderers.
Stream Markdown
Basic Usage
Stream Markdown
Loading demo...
Interaction Contract
- The document is split into blocks and rendered per block, so appending text does not reflow the whole page — the property that makes this usable while streaming.
- While
streamingis true a tail cursor is shown, and an unclosed tail fence has its rendering deferred: a half-written code block does not flash in a wrong form first, it waits for the closing fence. fenceClosedisfalseonly for the still-growing tail fence of a streaming document. Indented code has no fence and also reportsfalse; any non-tail block is treated as complete.sanitizeis on by default and loads dompurify through a dynamicimport(). Keep it on — the content is model output.- If dompurify fails to load, the sanitizer is left null and the component keeps working. Sanitization is therefore best-effort and is not a substitute for a server-side trust boundary.
- Each instance owns its own
Marked(withgfmandbreaks) rather than mutating the global singleton, which would reconfigure every other consumer in the app. renderersare matched by fence language — the first word, lowercased. Unregistered languages fall back to the default code block.- A registered renderer receives
StreamMarkdownBlockContextas props, includingfenceClosed, so it can show a placeholder until the block completes. themeacceptslight,darkorauto, withautofollowing the environment.- The bundled GitHub-Markdown stylesheet is a global import, so every rule in it is scoped to
:where(.tx-markdown-view, .tx-stream-md)..markdown-bodyis a very generic class name — before the scope, importing this component restyled any host page that used it for its own prose.:where()adds no specificity, so the scope confines the sheet without changing how its rules compete with each other.
API
Props
| Name | Type | Default | Description |
|---|---|---|---|
content | string | — | The Markdown source. Required. |
streaming | boolean | false | Whether output is still arriving; drives the tail cursor and deferred tail-fence rendering. |
sanitize | boolean | true | Whether to sanitize rendered HTML through dompurify. |
theme | 'light' | 'dark' | 'auto' | 'auto' | Colour theme. |
renderers | Record<string, StreamMarkdownBlockRenderer> | — | Per-language fenced-block renderers, e.g. { mermaid: TxMermaidBlock }. |
Events
TxStreamMarkdown emits no component events.
Slots
TxStreamMarkdown exposes no slots. Register a component through renderers to take over a particular kind of block.
Best Practices
- Do not turn
sanitizeoff. Model output is untrusted content and this is where it becomes DOM. - Keep
streamingtrue for the duration and set it false when generation ends, or the cursor stays at the tail forever. - Handle
fenceClosed === falsein custom renderers with a loading state rather than parsing a partial block. - Key
renderersby the lowercase language that opens the fence (```mermaid→mermaid). - Use
TxMarkdownViewfor static Markdown; it does not need the block-splitting and cursor machinery.