Skip to content

Vue AI chat Markdown renderer

markstream-vue is the Vue 3 renderer for AI chat surfaces where Markdown is visible while an LLM response is still streaming. It is useful when the message can contain code fences, tables, Mermaid diagrams, KaTeX math, HTML-like tags, or long reasoning output before the final chunk arrives.

Install

bash
pnpm add markstream-vue
ts
import MarkdownRender from 'markstream-vue'
import 'markstream-vue/index.css'

Use index.px.css instead of index.css for mobile WebViews where the host app changes root font scaling.

Minimal Vue chat message

vue
<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"
    :fade="false"
  />
</template>

The two important props are content and final. Keep appending text to content while the stream is active. Set final to true only when the model has finished, so open fences, tables, math, and heavy blocks can settle into their final rendering.

SSE chat stream example

vue
<script setup lang="ts">
import MarkdownRender from 'markstream-vue'
import { onBeforeUnmount, onMounted, ref } from 'vue'
import 'markstream-vue/index.css'

const content = ref('')
const isDone = ref(false)

let source: EventSource | null = null

onMounted(() => {
  source = new EventSource('/api/chat/stream')
  source.onmessage = (event) => {
    if (event.data === '[DONE]') {
      isDone.value = true
      source?.close()
      return
    }

    const data = JSON.parse(event.data) as { content?: string }
    content.value += data.content ?? ''
  }
})

onBeforeUnmount(() => {
  source?.close()
})
</script>

<template>
  <MarkdownRender
    mode="chat"
    :content="content"
    :final="isDone"
    smooth-streaming="auto"
    :fade="false"
  />
</template>

What this solves in AI chat

Streaming stateWhat users should see
Unclosed ```ts fenceA stable readable code block state, not the rest of the message swallowed unexpectedly
Partial table syntaxText that does not flicker between paragraph and table
Partial KaTeXSource text until the expression is complete enough to render
Mermaid diagram still growingProgressive rendering when the diagram syntax becomes stable
Long reasoning answerBounded live nodes and viewport-aware heavy blocks when virtualization is enabled

Safe HTML policy

LLM output is external content. Start with escaped or safe HTML unless your server fully controls the Markdown.

vue
<MarkdownRender
  mode="chat"
  :content="content"
  :final="isDone"
  html-policy="escape"
/>

Use custom components for trusted tags such as thinking when you want model-specific UI without enabling raw HTML.

When to avoid markstream-vue

Use a smaller static renderer when the content is short, complete before display, and never includes heavy blocks or long streaming states. markstream-vue earns its weight when the user reads the answer while it is still being written.

Next steps

FAQ

Does markstream-vue work with SSE and WebSocket chat streams?

Yes. Append streamed chunks to a growing content string, keep final false while the answer is still arriving, and set final true when the stream ends.

Can it render an unclosed code fence while the model is still writing?

Yes. markstream-vue keeps incomplete Markdown states readable, including unclosed fences, partial tables, math, and HTML-like tags.

Should I use markstream-vue for static Vue Markdown?

Use a simpler Markdown renderer for short static Markdown. Use markstream-vue when AI chat streaming, incomplete states, heavy blocks, or long responses matter.