Choosing a Markdown Library for AI Chat in 2026

Compare Streamdown, Markstream, Comark, @humanspeak/svelte-markdown, markdown-to-jsx, and plain parsers for rendering streamed LLM output. See how each handles incomplete Markdown, components, security, and frameworks.

TL;DR: For a React-only chat where the model writes standard Markdown, start with Streamdown. If the model must place components, or you ship Vue, Svelte, Angular, or a terminal UI, use Comark. For Vue 2 or very long documents that need virtualization, look at Markstream. For a Svelte-only app built on marked extensions, use @humanspeak/svelte-markdown.

This guide is part of Comark's documentation. Each claim about another library links to that library's repository or documentation, so you can check it.

What an AI chat renderer needs

A model sends its answer a few characters at a time. A renderer built for complete documents re-parses the whole text on each update. Between updates, it shows broken output. Five questions separate the options:

  • Incomplete syntax: what does the reader see while **bold, a code fence, a link, or a table is still open? The renderer can complete the syntax, hide it, or show it raw.
  • Re-render cost: does each new token re-parse and re-render the whole message, or only the last block? This matters for long answers.
  • Components in the output: can the model place a chart, a product card, or a tool result? Can that component take props and nested content?
  • Security of model output: model output is untrusted input. Check what happens to raw <script>, on* handlers, and javascript: links by default.
  • Framework: a renderer for one framework locks the chat UI to it. Check which frameworks have a stable release.

The options

Streamdown

Streamdown is a React component library by Vercel, built on unified and remark. It positions itself as a drop-in replacement for react-markdown, and it powers the AI SDK's AI Elements.

  • Incomplete syntax: its remend preprocessor completes unterminated syntax at the string level, before parsing. Incomplete links point to streamdown:incomplete-link.
  • Re-render cost: it splits the text into blocks and memoizes them.
  • Chat features: animation, carets, copy and download buttons, table export, and a confirmation modal for links.
  • Components: a react-markdown style components map. Custom HTML tags need allowedTags, or the sanitizer strips them.
  • Security: rehype-sanitize with the GitHub schema, plus rehype-harden, are on by default.
  • Tradeoffs: React only. Code highlighting, math, Mermaid, and CJK support are separate @streamdown/* plugins.

Markstream

Markstream has packages for Vue 3, React, Svelte 5, Angular 20+, and Vue 2. Its parser is built on markdown-it-ts.

  • Incomplete syntax: unclosed constructs render as "loading" nodes until you set the final prop.
  • Re-render cost: a token cache, render modes, batching, and virtualization with max-live-nodes help with long documents.
  • Chat features: smooth streaming, typewriter and fade effects, Mermaid and KaTeX in a worker, and optional D2, AntV Infographic, and diff blocks.
  • Components: setCustomComponents() replaces node renderers. Custom HTML-like tags such as <thinking> can map to components.
  • Security: htmlPolicy="safe" is the default, and content is sanitized mid-stream too.
  • Tradeoffs: its own docs list Vue as stable, React as beta, Svelte as beta or experimental, and Angular as alpha. Its component mechanisms are node overrides, custom tags, and placeholders such as [[CALLOUT:warning title="..."]], not a syntax with named slots.

Comark

Comark is an open-source Markdown parser and renderer with components, attributes and plugins. It streams AI output to React, Vue, Svelte, Angular, HTML and the terminal.

  • Incomplete syntax: autoClose completes unterminated syntax on every parse, including components, tables, and frontmatter. Incomplete links point to comark:incomplete-link.
  • Re-render cost: the parser's streaming option reuses the previous output when the new text appends to the old text. It also drops a half-typed opener at the end. The Vue, Svelte, and Angular renderers pass their streaming prop to the parser.
  • Components: the model writes component syntax such as ::flight-card{from="CDG"}, with props and named slots. The renderer mounts only components you registered.
  • Tradeoffs: the default html plugin keeps raw <script>, so add the security plugin with a blockedTags list for model output. The parser bundle is larger than marked or markdown-it. There is no remark or rehype plugin support.

@humanspeak/svelte-markdown

@humanspeak/svelte-markdown is the Svelte 5 successor to the original svelte-markdown. It is built on marked and htmlparser2.

  • Incomplete syntax: it buffers half-open tags. Its documentation doesn't describe completing unterminated inline syntax.
  • Re-render cost: it re-lexes only the open block and coalesces updates once per animation frame. writeChunk() accepts out-of-order chunks.
  • Components: any HTML tag, such as <thinking>, can map to a component through renderers.html.
  • Security: a URL allowlist, on* and srcdoc stripping, and escaped script and style are on by default. Its docs list gaps, for example iframe, form, and embed still render.
  • Tradeoffs: Svelte only. Streaming turns off when you use an async extension such as Mermaid.

markdown-to-jsx

markdown-to-jsx has zero dependencies. It outputs React, React Native, Solid, Vue, HTML, and Markdown.

  • Incomplete syntax: the optimizeForStreaming option hides incomplete syntax until the closing delimiter arrives. It is off by default.
  • Components: PascalCase JSX tags in the content render as your components, with parsed array, object, and boolean props.
  • Security: its tag filter and raw HTML sanitization are on by default.
  • Tradeoffs: there is no plugin system; you customize output with overrides and renderRule.

Plain parsers (marked, markdown-it, remark)

marked, markdown-it, and remark are mature, fast parsers with large plugin ecosystems. None of them handles incomplete Markdown. For a chat UI, you add your own completion and block splitting.

Comparison

LibraryFrameworksIncomplete MarkdownComponents in outputSanitization default
StreamdownReactremend completes syntax before parsingcomponents map; custom tags need allowedTagsrehype-sanitize (GitHub schema) and rehype-harden
MarkstreamVue 3, React (beta), Svelte 5 (beta), Angular 20+ (alpha), Vue 2Loading nodes until finalsetCustomComponents(), custom HTML-like tagshtmlPolicy="safe"
ComarkVue, React, Svelte, Angular, HTML, ANSIautoClose completes syntax on every parse::component{props} with named slotsUnsafe Markdown link URLs aren't linked; raw HTML, including <script>, is kept until you add the security plugin with blockedTags
@humanspeak/svelte-markdownSvelte 5Re-lexes the open block, buffers half-open tagsrenderers.html.<tag> for any tagURL allowlist, on* and srcdoc stripped, script and style escaped
markdown-to-jsxReact, React Native, Solid, Vue, HTMLoptimizeForStreaming hides it (opt-in)PascalCase JSX tags with parsed propsTag filter and raw HTML sanitization on
marked, markdown-it, remarkAny (HTML string or AST)NoneNone built inmarked: none; markdown-it and remark: raw HTML off

How to choose

Start with the constraint that you can't change, usually the framework, and then check the other needs:

  • React-only chat, standard Markdown, no component syntax: Streamdown fits. Animation, carets, and copy buttons are built in. See Comark vs Streamdown.
  • Components in model output: Comark fits. The model writes ::component{props} text, and the renderer mounts only the components you registered. See generative UI.
  • Vue, Svelte, Angular, or terminal output: Comark has a renderer for each. One parsed document renders in all of them.
  • Vue 2, or very long documents that need virtualization: Markstream fits. See Comark vs Markstream.
  • Svelte-only, with marked extensions: @humanspeak/svelte-markdown fits. See Comark vs svelte-markdown.
  • React Native or Solid: markdown-to-jsx has entry points for both. See Comark vs markdown-to-jsx.
  • Server-side HTML only, no streaming: a plain parser with a sanitizer can be enough.

Whatever you pick, treat the model's output as untrusted input. Check the sanitization defaults in the table, and test with a response that contains <script> and a javascript: link.

FAQ

Next steps

© 2026 Vercel, Inc.