npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@wooshiiltd/streamdown-vue

v0.1.1

Published

A Vue 3 port of streamdown — markdown rendering designed for AI-powered streaming.

Readme

streamdown-vue

A Vue 3 markdown renderer designed for AI-powered streaming — the Vue port of streamdown.

Overview

Formatting Markdown is easy. Tokenized, partially-streamed Markdown is not. streamdown-vue is built specifically for streaming AI output: it splits markdown into stable blocks, repairs broken tokens (unfinished **bold, dangling [link](, half-typed <table>s), and only re-renders the last incomplete block as new tokens arrive. The other blocks stay referentially stable and skip re-render entirely.

It mirrors the React streamdown package's public surface where the shape transfers, swapping React components for Vue 3 ones (Composition API, defineComponent, <script setup>-friendly).

Features

  • Streaming-optimized. Block-based incremental parsing; sibling blocks aren't re-rendered when new tokens land in the last block.
  • Broken-markdown repair. remend patches incomplete tokens during the stream so partial output still renders cleanly.
  • GitHub Flavored Markdown. Tables, task lists, strikethrough, footnotes.
  • Math (LaTeX via KaTeX). Optional @streamdown/math plugin.
  • Mermaid diagrams. Optional @streamdown/mermaid plugin with pan-zoom + fullscreen + PNG/SVG/MMD export.
  • Code syntax highlighting (Shiki). Optional @streamdown/code plugin. Light + dark themes, line numbers, copy + download buttons.
  • CJK-friendly. Optional @streamdown/cjk plugin with autolink boundary fixes.
  • Link safety. Modal that confirms before opening external links; pluggable onLinkCheck for allowlists.
  • Sanitization. rehype-sanitize + rehype-harden defaults; allowedTags for custom HTML.
  • RTL detection. Per-block first-strong-character algorithm for mixed-direction documents.
  • Tailwind v4 prefix support. prefix="tw" prepends tw: to every utility class.
  • i18n. Override every UI label via translations.
  • Custom renderers. Plug a Vue component in for any code-fence language (Graphviz, dot, custom diagrams).

Installation

pnpm add @wooshiiltd/streamdown-vue vue
# or: npm i @wooshiiltd/streamdown-vue vue

Optional plugin packages:

pnpm add @streamdown/code shiki              # syntax highlighting
pnpm add @streamdown/math katex              # LaTeX equations
pnpm add @streamdown/mermaid mermaid          # diagrams
pnpm add @streamdown/cjk                      # CJK text handling

Tailwind setup

Tell Tailwind to scan the streamdown-vue package for utility classes. In your project's main CSS file:

@source "../node_modules/@wooshiiltd/streamdown-vue/dist/*.js";

Add matching @source lines for any plugin packages you installed:

@source "../node_modules/@streamdown/code/dist/*.js";
@source "../node_modules/@streamdown/math/dist/*.js";
@source "../node_modules/@streamdown/mermaid/dist/*.js";
@source "../node_modules/@streamdown/cjk/dist/*.js";

In a monorepo with hoisted node_modules, adjust the relative path. From apps/web/src/styles/main.css to a hoisted root:

@source "../../../../node_modules/@wooshiiltd/streamdown-vue/dist/*.js";

Stylesheet

Import the bundled animation stylesheet once at app entry:

// main.ts
import "@wooshiiltd/streamdown-vue/styles.css";

If you use the math plugin, also import KaTeX's CSS:

import "katex/dist/katex.min.css";

Design tokens (CSS custom properties)

Components use a shadcn-style design token system. If you already use shadcn-vue, the variables are set up. Otherwise, add a 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;
}

Usage

Minimal example

<script setup lang="ts">
import { ref } from "vue";
import { Streamdown } from "@wooshiiltd/streamdown-vue";
import "@wooshiiltd/streamdown-vue/styles.css";

const markdown = ref(`# Hello

A **streaming** markdown renderer with [links](https://example.com), \`inline code\`, and:

- alpha
- beta
- gamma

\`\`\`ts
function greet(name: string) {
  return \`hello, \${name}\`;
}
\`\`\`
`);
</script>

<template>
  <Streamdown :is-animating="false">{{ markdown }}</Streamdown>
</template>

Streaming from an LLM

streamdown-vue doesn't ship its own chat hook — give it the current text and toggle is-animating while tokens are still arriving:

<script setup lang="ts">
import { ref } from "vue";
import { Streamdown } from "@wooshiiltd/streamdown-vue";
import { code } from "@streamdown/code";
import { mermaid } from "@streamdown/mermaid";
import { math } from "@streamdown/math";
import "@wooshiiltd/streamdown-vue/styles.css";
import "katex/dist/katex.min.css";

const text = ref("");
const isAnimating = ref(false);

async function send(prompt: string) {
  text.value = "";
  isAnimating.value = true;
  const res = await fetch("/api/chat", {
    method: "POST",
    body: JSON.stringify({ prompt }),
  });
  const reader = res.body!.getReader();
  const decoder = new TextDecoder();
  while (true) {
    const { value, done } = await reader.read();
    if (done) break;
    text.value += decoder.decode(value, { stream: true });
  }
  isAnimating.value = false;
}
</script>

<template>
  <Streamdown
    animated
    :is-animating="isAnimating"
    :plugins="{ code, mermaid, math }"
    caret="block"
  >{{ text }}</Streamdown>
</template>

The animated flag fades in new word-spans as they arrive (already-rendered words skip re-animation). caret="block" shows a blinking caret at the end of the streamed output, hidden automatically inside incomplete code fences and tables.

Custom renderer for a language

Drop in a Vue component for any code-fence language:

<script setup lang="ts">
import { defineComponent, h } from "vue";
import { Streamdown, type CustomRenderer } from "@wooshiiltd/streamdown-vue";

const Graphviz = defineComponent({
  props: { code: String, language: String, isIncomplete: Boolean },
  setup(props) {
    return () =>
      h("div", { class: "graphviz" }, props.isIncomplete ? "…" : props.code);
  },
});

const renderers: CustomRenderer[] = [
  { language: "dot", component: Graphviz },
];

const md = `\`\`\`dot\ndigraph { a -> b }\n\`\`\``;
</script>

<template>
  <Streamdown :plugins="{ renderers }">{{ md }}</Streamdown>
</template>

Static mode (non-streaming)

For one-shot rendering with no streaming features:

<Streamdown mode="static">{{ markdown }}</Streamdown>

This skips block splitting + remend repair and renders the entire document as one tree.

API

<Streamdown> props

| prop | type | default | notes | |---|---|---|---| | children | string | "" | Markdown source — canonical input. The default-slot <Streamdown>{{ md }}</Streamdown> form also works for static usage, but for streaming where the source mutates in place (e.g. AI SDK / Pinia store doing part.text += chunk), pass via :children to guarantee reactivity. | | mode | "static" \| "streaming" | "streaming" | Static mode renders one tree; streaming splits into blocks. | | dir | "auto" \| "ltr" \| "rtl" | — | "auto" runs first-strong-character detection per block. | | isAnimating | boolean | false | True while tokens are still arriving. Drives caret + animation skip. | | animated | boolean \| AnimateOptions | — | Fade in word-spans as they arrive. Pass { animation, duration, easing, sep, stagger } for tuning. Default stagger: 40 (matches React) queues a delay per token, so a 100-word block keeps animating for ~4s after content stops growing. Drop to stagger: 0 if you don't want that tail on multi-block streams. | | caret | "block" \| "circle" | — | Trailing caret while streaming. | | parseIncompleteMarkdown | boolean | true | Run remend repair before block splitting. | | normalizeHtmlIndentation | boolean | false | Strip 4+ space indentation that would otherwise become a code block. | | plugins | PluginConfig | — | { code?, math?, mermaid?, cjk?, renderers? }. | | controls | ControlsConfig | true | false to hide all action buttons; nested config per type (code, table, mermaid). | | linkSafety | LinkSafetyConfig | { enabled: true } | Toggle the safety modal; provide onLinkCheck(url) => boolean \| Promise<boolean> for allowlists. | | mermaid | MermaidOptions | — | { config?, errorComponent? } for diagram tuning + custom error UI. | | lineNumbers | boolean | true | Code block line numbers. Per-fence override via noLineNumbers meta string. | | shikiTheme | [ThemeInput, ThemeInput] | ["github-light", "github-dark"] | Light + dark Shiki themes. | | allowedTags | Record<string, string[]> | — | Custom HTML tags + their permitted attributes. Pairs with literalTagContent. | | literalTagContent | string[] | — | Tag names whose children render as plain text (no markdown). | | translations | Partial<StreamdownTranslations> | — | Override every UI string. | | icons | Partial<IconMap> | — | Replace any of the 10 default SVG icons with your own component. | | prefix | string | — | Tailwind v4 prefix() support. prefix="tw" produces tw:flex etc. | | className | string | — | Class on the root container. | | components | Components | — | Override any default renderer ({ h1: MyHeading, code: MyCode, inlineCode: MyInlineCode, ... }). | | rehypePlugins / remarkPlugins | PluggableList | defaults | Replaces the default plugin chain (matches React upstream). The default rehype chain is rehypeRaw + rehypeSanitize + rehypeHarden; if you pass your own rehypePlugins, you also drop sanitization and link hardening, so include them yourself or you'll be rendering unsanitized HTML. | | BlockComponent | Component | Block | Replace the per-block renderer. | | parseMarkdownIntoBlocksFn | (md) => string[] | parseMarkdownIntoBlocks | Custom block splitter. | | onAnimationStart / onAnimationEnd | () => void | — | Fired when isAnimating flips. Suppressed in mode="static". |

Composables

import {
  useStreamdownContext,    // current ControlsConfig + isAnimating + theme + ...
  useTranslations,          // current StreamdownTranslations
  useIcons,                 // current IconMap
  useCn,                    // prefix-aware cn()
  usePlugins,               // current PluginConfig | null
  useCodePlugin,            // CodeHighlighterPlugin | null
  useMermaidPlugin,         // DiagramPlugin | null
  useMathPlugin,            // MathPlugin | null
  useCjkPlugin,             // CjkPlugin | null
  useCustomRenderer,        // CustomRenderer | null for a given language
  useIsCodeFenceIncomplete, // true inside the last block when it has an unclosed fence
  useCodeBlockContext,      // { code: string } from the nearest CodeBlock
  useDeferredRender,        // IntersectionObserver-based lazy renderer for heavy children
  useThrottledDebounce,     // ref<T> with throttle + trailing debounce
} from "@wooshiiltd/streamdown-vue";

Reusable building blocks

If you want pieces of the renderer without <Streamdown> orchestration:

import {
  Markdown,                    // raw markdown → VNode (no streaming)
  Block,                       // a single memoized block
  hastToVNode,                 // HAST tree → Vue VNode
  renderMarkdownToVNode,       // string → VNode (cached unified processor)
  parseMarkdownIntoBlocks,     // string → string[]
  detectTextDirection,         // "ltr" | "rtl"
  hasIncompleteCodeFence,
  hasTable,
  normalizeHtmlIndentation,
  defaultUrlTransform,
  defaultComponents,           // the renderer map
  CodeBlock, CodeBlockBody, CodeBlockContainer, CodeBlockHeader,
  CodeBlockCopyButton, CodeBlockDownloadButton, CodeBlockSkeleton,
  Mermaid, PanZoom,
  MermaidDownloadDropdown, MermaidFullscreenButton, svgToPngBlob,
  TableCopyDropdown, TableDownloadButton, TableDownloadDropdown,
  TableFullscreenButton,
  tableDataToCSV, tableDataToTSV, tableDataToMarkdown,
  extractTableDataFromElement, escapeMarkdownTableCell,
  ImageComponent, Anchor, LinkSafetyModal,
  animate, createAnimatePlugin,
} from "@wooshiiltd/streamdown-vue";

Differences from the React package

  • Component model. Vue 3 Composition API; defineComponent instead of React function components. Most renderers are functional defineComponent calls in .ts files (no .vue SFCs required).
  • Hooks → composables. useStateref, useMemocomputed, useEffectwatchEffect / onMounted / onBeforeUnmount, useContextinject. The public useXxx names match the React surface.
  • Context → provide/inject. Five InjectionKey<T> symbols (STREAMDOWN_CONTEXT_KEY, PLUGIN_CONTEXT_KEY, ICON_CONTEXT_KEY, PREFIX_CONTEXT_KEY, TRANSLATIONS_CONTEXT_KEY) plus BLOCK_INCOMPLETE_CONTEXT_KEY and CODE_BLOCK_CONTEXT_KEY. Streamdown provides reactive proxies for context, icons, translations; refs for plugins + cn function.
  • HAST → VNode. A custom hastToVNode walker replaces React's hast-util-to-jsx-runtime. It mirrors that library's prop-translation, custom-component, and key-passing semantics, calling Vue's h() instead of jsx/jsxs.
  • Block memoization. A shallowRef-backed cache keyed by [content, dir, isIncomplete, components, rehypePlugins, remarkPlugins, ...] mirrors React.memo's custom comparator. Sibling blocks skip re-render during streaming.
  • useTransition was dropped. Vue's reactivity already batches; the streaming path uses nextTick and ref updates directly.
  • Async components. defineAsyncComponent is used for the highlighted code body so Shiki loads on demand. Mermaid is eagerly imported (it ships as the diagram engine and was already in the main chunk via MermaidFullscreenButton).

Plugins

Five workspace packages are framework-agnostic and shared with the React package:

Pass them via the plugins prop:

import { code } from "@streamdown/code";
import { math } from "@streamdown/math";
import { mermaid } from "@streamdown/mermaid";
import { cjk } from "@streamdown/cjk";

Status

v0.1.0, first published cut. 222 tests covering every documented prop, the HAST renderer, all default components, all plugin integration paths, and the streaming + animation flow. Vue 3.4+ peer dep, ESM-only, ~25 KB gzipped.

License

Apache-2.0. Source at https://github.com/wooshiiltd/streamdown-vue.