vue3-streamdown
v0.1.3
Published
Vue 3 port of streamdown — a drop-in replacement for vue-markdown, designed for AI-powered streaming.
Downloads
162
Maintainers
Readme
Overview
Streaming Markdown from LLMs creates unique rendering challenges: incomplete code fences, partial tables, and unterminated blocks can all break a naive renderer. vue3-streamdown handles all of this gracefully — the same battle-tested approach as Vercel's streamdown, now for the Vue ecosystem.
Works with any AI streaming API: Vercel AI SDK, OpenAI, Anthropic, or raw SSE streams.
Features
- ⚡ Streaming-optimized — gracefully handles partial/incomplete Markdown from LLMs
- 🎨 Syntax highlighting — beautiful code blocks via Shiki
- 📊 GitHub Flavored Markdown — tables, task lists, strikethrough
- 📈 Mermaid diagrams — render flowcharts, sequence diagrams, and more
- 🔢 Math rendering — LaTeX equations via KaTeX
- 🌐 CJK support — proper word-wrapping for Chinese/Japanese/Korean
- 🛡️ Security-first — built-in HTML sanitization via
rehype-sanitize+rehype-harden - 🎯 Animated cursor — blinking block/circle caret during streaming
- 🔗 Link safety — external link confirmation modal
- 🌙 Dark mode — class-based dark mode (
.dark), compatible with shadcn/ui - 💪 Fully typed — complete TypeScript support with Composition API
Preview
Streaming with animated caret
┌─────────────────────────────────────────────────────┐
│ │
│ Here is a Fibonacci sequence in Python: │
│ │
│ ┌─────────────────────────────────────────────┐ │
│ │ python [⎘] [↓] │ │
│ │ 1 def fib(n): │ │
│ │ 2 if n <= 1: │ │
│ │ 3 return n │ │
│ │ 4 return fib(n-1) + fib(n-2) ▋ │ │
│ └─────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────┘Table with copy/download controls
┌──────────────────────────────────────────────────────────┐
│ Framework │ Stars │ License [⎘] [↓] [⛶] │
│──────────────┼─────────┼──────────────────────────────── │
│ Vue 3 │ 48k │ MIT │
│ React │ 226k │ MIT │
│ Svelte │ 80k │ MIT │
└──────────────────────────────────────────────────────────┘Mermaid diagram rendering
┌────────────────────────────────────────────────────────────┐
│ mermaid [↓▾] [⎘] [⛶] [🔍±] │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Client ──► API Server ──► Database │ │
│ │ ▲ │ │ │
│ │ └──────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘Installation
npm install vue3-streamdown
# or
pnpm add vue3-streamdown
# or
yarn add vue3-streamdownStyles are automatically injected when the library is imported — no additional CSS import or Tailwind setup required.
If you prefer to manage styles manually (e.g. for SSR or CSP reasons), you can import the pre-compiled CSS directly instead:
import "vue3-streamdown/style.css";
CSS Custom Properties (shadcn/ui design tokens)
Components use shadcn/ui CSS variables for theming. If you already use shadcn/ui these are set automatically. Otherwise, add the following minimal set to your global CSS:
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--border: oklch(0.922 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--sidebar: oklch(0.985 0 0);
--radius: 0.625rem;
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--border: oklch(0.269 0 0);
--primary: oklch(0.985 0 0);
--primary-foreground: oklch(0.205 0 0);
--sidebar: oklch(0.205 0 0);
--radius: 0.625rem;
}Dark mode
vue3-streamdown uses class-based dark mode: dark styles are applied when a .dark class is present on any ancestor element (typically <html>). This matches shadcn/ui's default behavior.
// Toggle dark mode
document.documentElement.classList.toggle("dark");Dark mode does not follow the OS
prefers-color-schememedia query automatically. If you want OS-based toggling, add/remove the.darkclass based onwindow.matchMedia('(prefers-color-scheme: dark)')yourself.
Usage
Basic usage
<script setup lang="ts">
import { Streamdown } from "vue3-streamdown";
</script>
<template>
<Streamdown :model-value="markdownText" />
</template>Note: Unlike the React version, content is passed via the
model-valueprop — not as slot content.
Streaming with Vercel AI SDK
<script setup lang="ts">
import { useChat } from "@ai-sdk/vue";
import { Streamdown } from "vue3-streamdown";
const { messages, status } = useChat();
</script>
<template>
<div v-for="message in messages" :key="message.id">
<template v-for="part in message.parts" :key="part.type">
<Streamdown
v-if="part.type === 'text'"
:model-value="part.text"
:is-animating="status === 'streaming'"
animated
caret="block"
/>
</template>
</div>
</template>Streaming with raw fetch / SSE
<script setup lang="ts">
import { ref } from "vue";
import { Streamdown } from "vue3-streamdown";
const content = ref("");
const isStreaming = ref(false);
async function chat(prompt: string) {
isStreaming.value = true;
content.value = "";
const response = await fetch("/api/chat", {
method: "POST",
body: JSON.stringify({ prompt }),
});
const reader = response.body!.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
content.value += decoder.decode(value);
}
isStreaming.value = false;
}
</script>
<template>
<button @click="chat('Explain quicksort with code')">Ask AI</button>
<Streamdown
:model-value="content"
:is-animating="isStreaming"
mode="streaming"
caret="block"
animated
/>
</template>With syntax highlighting (Shiki)
Install the code plugin:
npm install @streamdown/code<script setup lang="ts">
import { Streamdown } from "vue3-streamdown";
import { createCodePlugin } from "@streamdown/code";
const codePlugin = createCodePlugin();
</script>
<template>
<Streamdown :model-value="markdown" :plugins="{ code: codePlugin }" />
</template>With Mermaid diagrams
Install the mermaid plugin:
npm install @streamdown/mermaid<script setup lang="ts">
import { Streamdown } from "vue3-streamdown";
import { createMermaidPlugin } from "@streamdown/mermaid";
const mermaidPlugin = createMermaidPlugin();
</script>
<template>
<Streamdown :model-value="markdown" :plugins="{ mermaid: mermaidPlugin }" />
</template>With math (KaTeX)
Install the math plugin and KaTeX CSS:
npm install @streamdown/math katex<script setup lang="ts">
import { Streamdown } from "vue3-streamdown";
import { createMathPlugin } from "@streamdown/math";
import "katex/dist/katex.min.css";
const mathPlugin = createMathPlugin();
</script>
<template>
<Streamdown :model-value="markdown" :plugins="{ math: mathPlugin }" />
</template>All plugins together
<script setup lang="ts">
import { Streamdown } from "vue3-streamdown";
import { createCodePlugin } from "@streamdown/code";
import { createMermaidPlugin } from "@streamdown/mermaid";
import { createMathPlugin } from "@streamdown/math";
import "katex/dist/katex.min.css";
const plugins = {
code: createCodePlugin(),
mermaid: createMermaidPlugin(),
math: createMathPlugin(),
};
</script>
<template>
<Streamdown
:model-value="content"
:is-animating="isStreaming"
:plugins="plugins"
mode="streaming"
caret="block"
animated
/>
</template>Custom translations (i18n)
<template>
<Streamdown
:model-value="markdown"
:translations="{
copyCode: '코드 복사',
copied: '복사됨',
viewFullscreen: '전체 화면',
exitFullscreen: '닫기',
downloadFile: '파일 다운로드',
}"
/>
</template>Tailwind CSS prefix support
<template>
<!-- All internal Tailwind classes will be prefixed: tw:flex, tw:text-sm, etc. -->
<Streamdown :model-value="markdown" prefix="tw" />
</template>Props
| Prop | Type | Default | Description |
| ---------------------------- | --------------------------------- | --------------------------------- | -------------------------------------------------------------------------------------------------- |
| model-value | string | "" | Markdown content to render |
| mode | "streaming" \| "static" | "streaming" | Rendering mode |
| is-animating | boolean | false | Whether the stream is active |
| animated | boolean \| AnimateOptions | — | Enable token-by-token animation |
| caret | "block" \| "circle" | — | Blinking cursor style during streaming |
| controls | boolean \| ControlsConfig | true | Show code/table/mermaid action buttons |
| line-numbers | boolean | true | Show line numbers in code blocks |
| shiki-theme | [ThemeInput, ThemeInput] | ["github-light", "github-dark"] | Shiki [lightTheme, darkTheme] — first theme for light mode, second for dark mode (.dark class) |
| plugins | PluginConfig | — | code / mermaid / math / cjk plugins |
| remark-plugins | Pluggable[] | — | Custom remark plugins |
| rehype-plugins | Pluggable[] | — | Custom rehype plugins |
| components | Components | — | Override hast → Vue component map |
| translations | Partial<StreamdownTranslations> | — | Override UI strings |
| icons | Partial<IconMap> | — | Override toolbar icons |
| prefix | string | — | Tailwind CSS class prefix |
| link-safety | LinkSafetyConfig | { enabled: true } | External link confirmation |
| mermaid | MermaidOptions | — | Mermaid global config |
| dir | "auto" \| "ltr" \| "rtl" | — | Text direction (auto = per-block detection) |
| parse-incomplete-markdown | boolean | true | Use remend for streaming-safe parsing |
| normalize-html-indentation | boolean | false | Prevent indented HTML being treated as code |
| allowed-tags | Record<string, string[]> | — | Custom HTML tags to allow through sanitization |
| literal-tag-content | string[] | — | Tags whose children are treated as plain text |
| remend | RemendOptions | — | Options passed to remend |
| on-animation-start | () => void | — | Called when streaming starts |
| on-animation-end | () => void | — | Called when streaming ends |
Composables (for custom child components)
If you build custom components that need to access the streamdown context:
import {
useStreamdownContext, // shikiTheme, controls, isAnimating, etc.
useTranslations, // UI strings
useIcons, // toolbar icons
useCn, // prefix-aware cn()
usePlugins, // full plugin config
useCodePlugin, // code plugin
useMermaidPlugin, // mermaid plugin
useIsBlockIncomplete, // whether current block is mid-stream
useIsBlockCode, // whether <code> is inside a <pre>
useCodeBlock, // { code: ComputedRef<string> } for copy/download
useAnimate, // animate plugin instance
} from "vue3-streamdown";Comparison with streamdown (React)
| Feature | streamdown (React) | vue3-streamdown (Vue 3) |
| --------------------------- | -------------------- | ------------------------- |
| Streaming-safe parsing | ✅ | ✅ |
| Syntax highlighting (Shiki) | ✅ | ✅ |
| Mermaid diagrams | ✅ | ✅ |
| Math (KaTeX) | ✅ | ✅ |
| CJK support | ✅ | ✅ |
| Animated cursor | ✅ | ✅ |
| Link safety modal | ✅ | ✅ |
| Dark mode | ✅ | ✅ |
| Tailwind prefix | ✅ | ✅ |
| TypeScript | ✅ | ✅ |
| Composition API / hooks | React hooks | Vue composables |
| SSR | ✅ (Next.js) | ✅ (Nuxt) |
License
Apache-2.0 — based on streamdown by Vercel.
