---
title: Stream Markdown
description: A Markdown renderer built for streaming output.
category: AiReasoning
status: beta
since: 0.3.9
tags: [ai, markdown, streaming]
syncStatus: reviewed
verified: true
---

## Usage

### Basic
While streaming, an unclosed tail fence defers rendering instead of flashing in half-written.
::::TuffDemoWrapper{demo="StreamMarkdownStreamMarkdownDemo" code-lang="vue"}
---
code: |
  <script setup lang="ts">
  import ChartBlock from './ChartBlock.vue'

  const content = ref('# Heading\n\nGenerating…')
  </script>

  <template>
    <TxStreamMarkdown
      :content="content"
      streaming
      :renderers="{ chart: ChartBlock }"
    />
  </template>
---
::::

### Best Practices

- Keep `sanitize` on: model output is untrusted, and this is where it becomes DOM.
- Set `streaming` to false when generation ends, or the caret stays at the tail.
- In custom renderers, show a loading state while `closed` is false instead of parsing a partial block.
- Key `renderers` by the lowercase first word of the fence (` ```chart ` → `chart`).
- Use `TxMarkdownView` for static Markdown; it needs none of the block and caret machinery.

## API Reference

### Props

| Name | Type | Default | Description |
|------|------|---------|-------------|
| `content` | `string` | — | The Markdown source. Required. |
| `streaming` | `boolean` | `false` | Output is still arriving; drives the caret, the reveal, and deferred tail-fence rendering. |
| `reveal` | `'aurora' \| 'hue' \| 'blur' \| 'languid' \| 'none'` | `'aurora'` | How new text enters, with `TxStreamText`'s presets; `languid` keeps its slow blur but not its rise. |
| `caret` | `boolean` | `true` | Shows the Tuff caret at the write head while `streaming`; switched off, it goes at once. |
| `sanitize` | `boolean` | `true` | Sanitizes HTML through DOMPurify. |
| `theme` | `'light' \| 'dark' \| 'auto'` | `'auto'` | Color theme; `auto` follows the environment. |
| `renderers` | `Record<string, StreamMarkdownBlockRenderer>` | — | Block renderers by fence language; each receives `StreamMarkdownBlockContext` as props. |
| `blockRemoteImages` | `boolean` | `true` | Shows remote images (`http(s)://`, `//`) as a placeholder until the reader loads them. |
| `blockedImageText` | `string` | `'Remote image blocked'` | Title of the blocked-image placeholder. |
| `loadImageOnceText` | `string` | `'Load this image'` | Text of the button that loads only this image. |
| `allowSessionImagesText` | `string` | `'Allow for this conversation'` | Text of the button that allows every remote image for the conversation. |
| `copyTableText` | `string` | `'Copy CSV'` | Text of a table's copy-as-CSV button. |
| `copiedTableText` | `string` | `'Copied'` | Button text shown briefly after a successful copy. |

## Overview

- The document renders block by block, so appended text never reflows the whole page.
- While streaming, the still-growing tail fence defers its final rendering until it closes; renderers get `closed: false` only for that fence.
- Unregistered languages use the built-ins: `TxMermaidBlock` for `mermaid`, `TxCodeBlock` for the rest.
- The caret is `TxStreamText`'s: right after the last character of a paragraph, heading, or quote, otherwise on its own line below. Under reduced motion nothing enters and the caret rests.
- DOMPurify loads dynamically; if it fails, the component keeps working unsanitized, so sanitizing is best-effort and no substitute for a server-side trust boundary.
- Image consent is module-level and shared by every instance; call the exported `resetRemoteImagePolicy()` when the conversation changes.
- Each instance owns its own `Marked` (`gfm`, `breaks`); the GitHub Markdown sheet is scoped under `:where(.tx-md)`, leaving the host's `.markdown-body` alone.
