---
title: "Choosing a Markdown Library for AI Chat in 2026"
description: "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."
canonical_url: "https://comark.dev/compare/markdown-libraries-for-ai-chat"
---
# 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](https://github.com/vercel/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](https://github.com/Simon-He95/markstream-vue) 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](https://markstream.simonhe.me/llms.txt) 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`](https://comark.dev/reference/auto-close) 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](https://comark.dev/syntax/components) 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](https://comark.dev/plugins/built-in/security) 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](https://github.com/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](https://github.com/quantizor/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](https://github.com/markedjs/marked), [markdown-it](https://github.com/markdown-it/markdown-it), and [remark](https://github.com/remarkjs/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.

- **marked** returns an HTML string and doesn't sanitize it. Its docs recommend a library such as DOMPurify. See [Comark vs marked](https://comark.dev/compare/comark-vs-marked).
- **markdown-it** disables raw HTML by default and rejects `javascript:` links. See [Comark vs markdown-it](https://comark.dev/compare/comark-vs-markdown-it).
- **remark** drops raw HTML unless you opt in, and recommends `rehype-sanitize`. See [Comark vs remark](https://comark.dev/compare/comark-vs-remark) and [Comark vs react-markdown](https://comark.dev/compare/comark-vs-react-markdown).

## Comparison

| Library                     | Frameworks                                                       | Incomplete Markdown                             | Components in output                             | Sanitization default                                                                                                                  |
| --------------------------- | ---------------------------------------------------------------- | ----------------------------------------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- |
| Streamdown                  | React                                                            | `remend` completes syntax before parsing        | `components` map; custom tags need `allowedTags` | rehype-sanitize (GitHub schema) and rehype-harden                                                                                     |
| Markstream                  | Vue 3, React (beta), Svelte 5 (beta), Angular 20+ (alpha), Vue 2 | Loading nodes until `final`                     | `setCustomComponents()`, custom HTML-like tags   | `htmlPolicy="safe"`                                                                                                                   |
| Comark                      | Vue, React, Svelte, Angular, HTML, ANSI                          | `autoClose` completes syntax on every parse     | `::component{props}` with named slots            | Unsafe Markdown link URLs aren't linked; raw HTML, including `<script>`, is kept until you add the security plugin with `blockedTags` |
| @humanspeak/svelte-markdown | Svelte 5                                                         | Re-lexes the open block, buffers half-open tags | `renderers.html.<tag>` for any tag               | URL allowlist, `on*` and `srcdoc` stripped, `script` and `style` escaped                                                              |
| markdown-to-jsx             | React, React Native, Solid, Vue, HTML                            | `optimizeForStreaming` hides it (opt-in)        | PascalCase JSX tags with parsed props            | Tag filter and raw HTML sanitization on                                                                                               |
| marked, markdown-it, remark | Any (HTML string or AST)                                         | None                                            | None built in                                    | marked: 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](https://comark.dev/compare/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](https://comark.dev/use-cases/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](https://comark.dev/compare/comark-vs-markstream).
- **Svelte-only, with marked extensions**: @humanspeak/svelte-markdown fits. See [Comark vs svelte-markdown](https://comark.dev/compare/comark-vs-svelte-markdown).
- **React Native or Solid**: markdown-to-jsx has entry points for both. See [Comark vs markdown-to-jsx](https://comark.dev/compare/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

::accordion
  :::accordion-item{label="Can I use Streamdown's remend with another renderer?"}
  Yes. `remend` is a separate preprocessor that completes unterminated syntax in a string. Comark has the same role in `autoCloseMarkdown()` from `comark`, which you can also call on its own. See [auto-close](https://comark.dev/reference/auto-close).
  :::

  :::accordion-item{label="Is completing syntax better than hiding it?"}
  Each has a cost. Completing shows formatting at once, but a guess can change when the real closing arrives. Hiding never shows a wrong guess, but text appears later and in bigger steps. Try both on your own model output.
  :::

  :::accordion-item{label="Do I need a sanitizer if the model is mine?"}
  Yes. A prompt injection in user input or in a retrieved document can make the model write `<script>` or a `javascript:` link. Treat every response as untrusted input.
  :::
::

## Next steps

- Build a streaming chat UI in your framework with [AI chat streaming](https://comark.dev/use-cases/ai-chat-streaming).
- Let the model render your components with [generative UI](https://comark.dev/use-cases/generative-ui).
- Watch `autoClose` complete partial Markdown in the [streaming playground](https://comark.dev/stream).

---

- [AI chat streaming](https://comark.dev/use-cases/ai-chat-streaming)
- [Streaming playground](https://comark.dev/stream)


## Sitemap

See the full [sitemap](https://comark.dev/sitemap.md) for all pages.
