Skip to main content

React Markdown Kit vs Streamdown for AI chat

Streamdown is Vercel's Markdown component for AI output. Its readme presents it as a replacement for react-markdown designed for AI-powered streaming, and it powers the AI Elements message component. @react-markdown-kit/renderer is a general React Markdown renderer with tests that pin down its streaming behaviour. They solve the same problem, but Streamdown ships a lot more of the finished UI.

The sections are in the same order as the other comparison pages (install size, GFM, security defaults, streaming, server rendering, plugins, migration). Facts about Streamdown come from its readme and its published package at version 2.6.0, vercel/streamdown. Facts about the kit link to the test or the generated table that produced them.

Install size​

Bytes a browser downloads for one import, measured from published tarballs by scripts/compare-bundles.mjs and committed to docs/data/bundle-sizes.json on 2026-09-20. React and React DOM are external in every row, CSS is counted separately and never added into the JavaScript number, and 1 KB is 1024 bytes.

One import, minified and gzipped
ImportVersionMinifiedGzippedLicensePeer react
@react-markdown-kit/renderer, CommonMark0.1.0118.5 KB36.8 KBMIT>=18
@react-markdown-kit/renderer + GFM preset0.1.0158.1 KB48.5 KBMIT>=18
streamdown2.6.0506.0 KB152.2 KBApache-2.0^18.0.0 || ^19.0.0

The kit with GFM is 103.7 KB smaller gzipped than Streamdown for the entry that was measured, import { Streamdown } from 'streamdown'. That's mostly a difference in scope. Streamdown's published dependencies include marked, remend, remark-gfm, rehype-raw, rehype-sanitize, rehype-harden, clsx and tailwind-merge, and the component ships styled output. The kit renders unstyled elements and leaves the rest to you.

Streamdown is also the only package compared here that isn't MIT (the measured tarball declares Apache-2.0).

On the Streamdown side there are two more costs that don't show up in the JavaScript number, both from its installation instructions: a Tailwind @source line pointing at node_modules/streamdown/dist/*.js, and a set of shadcn/ui CSS custom properties without which, the readme says, components may render with missing backgrounds or borders. The kit emits no class names unless you ask for them (styling, docs/STYLING.md).

GFM​

Both render GitHub Flavored Markdown. Streamdown lists tables, task lists and strikethrough as built-in features and depends on remark-gfm. On the kit it's one extra line.

import Markdown, { defineMarkdownPreset, gfm } from '@react-markdown-kit/renderer'

const preset = defineMarkdownPreset({ extensions: [gfm()] })

<Markdown preset={preset}>{content}</Markdown>

The kit's GFM is asserted by tests/gfm.test.ts: 68 cases derived from the GFM spec, each run through the native gfm() extension, through remarkPlugins={[remarkGfm]}, and through plain CommonMark, where the first two must agree after normalizing attribute order, entity spelling and whitespace between block tags (tests/helpers/html.ts) and the third must not produce the GFM markup, unless the case is one of the 12 that assert something is not GFM (those are named in the test so the exemption can't spread). CommonMark itself is covered example by example in tests/commonmark.test.ts. No conformance suite is run here against Streamdown.

Streamdown also has things the renderer doesn't include: KaTeX math, Shiki syntax highlighting, and Mermaid, each as an optional @streamdown/* plugin package in 2.6.0. The kit has a Mermaid plugin of its own that draws flowcharts and sequence diagrams and doesn't load Mermaid.js (Mermaid in React Markdown). It doesn't ship math or syntax highlighting, so you'd add a rehype plugin for those.

Security defaults​

Streamdown's readme lists "Security-first" and mentions rehype-harden, and its published dependencies include rehype-raw, rehype-sanitize and rehype-harden. So raw HTML goes through a sanitizer instead of being left inert. This page doesn't test that pipeline, so all it reports is the dependency list and what the readme says.

On the kit, raw HTML never becomes markup until you opt in.

DefaultReact Markdown Kit
Raw HTML in the sourceVisible escaped text. skipHtml: true removes it. Neither executes it
Opting inrehype-raw then rehype-sanitize, in that order, passed by you
URLshttp, https, irc, ircs, mailto, xmpp allowed, everything else emptied
A precompiled documentSame policy, so it is not a bypass

Each row is a test in the renderer security tests, and the model is written up in the security model.

For model output, the thing to look at is what a hallucinated <script> or javascript: link does. On the kit it ends up as text and an empty href, and you don't have to install or configure anything for that.

Streaming​

Streaming is what Streamdown was built for. Streamdown has the longer feature list here, and the kit has the published tests.

Streamdown's readme describes streaming-optimized rendering with unterminated block parsing built on remend, and memoized rendering for efficient updates. It also exposes animated and isAnimating props for the AI SDK's streaming status.

React Markdown Kit handles this without any option. packages/renderer/tests/streaming.test.tsx is 59 tests. Seven documents are fed token by token, where words stay whole and every whitespace and punctuation character is its own token, so a fence arrives as three separate backticks. Every prefix is asserted on four properties:

  1. no prefix throws,
  2. the HTML of the closed prefix is byte-identical at every later prefix,
  3. nothing from the closed prefix is duplicated,
  4. the last prefix renders exactly like the whole document rendered once.

The cases are an unclosed code fence, half-written emphasis and strong, a table mid-row, a list mid-item, a heading with no trailing newline, a link with an unclosed bracket, and a fenced block that closes late. The same run is repeated through compileMarkdown, and a mounted React root is grown token by token to check that a precompiled document re-renders as it grows and ends byte-identical to a root that only ever saw the finished document. One test checks that the heading element is reused as the document grows instead of being recreated.

Half-written constructs fall back to the characters typed so far: **str is the text **str, [text]( is the text [text](, and a table is a paragraph until its delimiter row is complete. An open fence never leaks its contents as markup, so a model writing Markdown inside a fence won't flash headings. Two prefixes do rewrite output that already rendered, and both are pinned by tests: a paragraph becomes a heading when a setext underline arrives, and a GFM autolink points at the truncated host while the URL is still being typed.

A prefix a model would produce mid-answer, rendered on this page
Markdown (edit me)
Rendered

Here is the summary you asked for.

MetricValue
tokens812
const partial = {
  answer: 42,

The fence is still open. Its contents render as code, the finished table above it is untouched, and deleting characters from the end never throws.

Where they differ is what an incomplete construct looks like. Streamdown completes it through remend, which closes an open **, ~~ or ` and swaps an unfinished link destination for streamdown:incomplete-link (their features list), so half-typed syntax renders as finished formatting that changes when the real delimiter arrives. The kit shows the literal characters and guarantees the closed prefix above doesn't move. If you want completion, run remend over the string before you pass it to the kit (the kit doesn't do that for you).

Server rendering​

Streamdown's published dist/index.js begins with "use client", so the component runs on the client. Its documented usage is a React client component driven by the AI SDK's useChat.

The kit renders on the server with no DOM. Its conformance suites call renderToStaticMarkup, and scripts/pack-check.mjs installs the packed tarball into a consumer with no Lexical and renders there (server rendering). The same component works in a React server component, during SSR and in a static build, which is useful if the chat transcript is also a page you want indexed.

The other server-side difference is compileMarkdown. A MarkdownDocument compiled once renders many times without parsing again: on the reference machine in benchmarks/README.md a 10 KB document takes 16.80 ms from a string and 5.99 ms precompiled, 2.8x faster, because parsing is about two thirds of the work. A finished chat message is a good fit for it.

On the other side, measured against react-markdown@10.1.0 (not Streamdown), 1 KB is 1.21x slower (2.69 ms against 2.23 ms), 10 KB is at parity (16.22 ms against 16.25 ms), 100 KB is 0.89x (206.96 ms against 233.36 ms). Short messages are the 1 KB case, and that row is an open regression tracked in benchmarks/README.md. Reproduce with pnpm bench. No benchmark against Streamdown is published here.

Plugins​

Streamdown takes a plugins object of @streamdown/* packages: code, mermaid, math, cjk in the readme example. Each is installed separately and each adds its own Tailwind @source line.

The kit takes remarkPlugins, rehypePlugins and remarkRehypeOptions, compared prop for prop against react-markdown at 4 of 4, 3 of 3 and 3 of 3 identical (docs/COMPATIBILITY.md), so anything in the unified ecosystem works. It also has extensions, which register micromark syntax, mdast handlers, hast handlers and editor behaviour in one declaration, so the renderer, the editor and a compile-only server read the same thing (extensions).

So structurally, Streamdown gives you a finished chat surface, while the kit gives you a document contract that the renderer, the editor (editor), the Mermaid plugin and the template plugin all share, and you assemble the surface.

Migration​

There is no codemod for this direction. rmk-migrate and rmk-compare handle react-markdown only (compare.mjs).

-import { Streamdown } from 'streamdown'
-import 'streamdown/styles.css'
+import Markdown, { defineMarkdownPreset, gfm } from '@react-markdown-kit/renderer'
+
+const preset = defineMarkdownPreset({ extensions: [gfm()] })

-<Streamdown>{part.text}</Streamdown>
+<Markdown preset={preset}>{part.text}</Markdown>

Here's what you give up and should plan for.

  • Styling. Streamdown ships styled output. The kit emits no class names by default, so you bring a stylesheet or the opt-in classNames hooks (styling).
  • Syntax highlighting and math. Shiki and KaTeX come with Streamdown's plugin packages. Here they are a rehype plugin you choose.
  • Mermaid. The kit's plugin renders flowcharts and sequence diagrams and doesn't load Mermaid.js (Mermaid in React Markdown).
  • animated and isAnimating. No equivalent. The kit re-renders whatever string you pass and guarantees the closed prefix does not move.

In exchange you get a smaller bundle, raw HTML that stays inert without a pipeline to configure, server rendering, a precompiled document path, and streaming behaviour covered by 59 tests you can run.

FAQ​

Which is smaller for an AI chat UI, Streamdown or React Markdown Kit?
React Markdown Kit is smaller. One import of @react-markdown-kit/renderer 0.1.0 with the GFM preset is smaller gzipped than one import of streamdown 2.6.0, measured by scripts/compare-bundles.mjs and committed to docs/data/bundle-sizes.json. To be fair, Streamdown's number includes syntax highlighting, styling and a hardened rehype pipeline.
Does React Markdown Kit handle partial Markdown from a model?
Yes, and there are tests for it. packages/renderer/tests/streaming.test.tsx is 59 tests that feed seven documents token by token and assert that no prefix throws, the closed prefix stays byte-identical, nothing is duplicated, and the last prefix matches a one-shot render.
Do I need Tailwind to render Markdown from an AI model?
Not with React Markdown Kit. It doesn't emit class names by default and there's no stylesheet you have to load. Streamdown documents a Tailwind @source directive and a set of shadcn/ui CSS custom properties as part of its installation.

Next​

Renderer playground · @react-markdown-kit/renderer on npm · Source on GitHub

react-markdown alternative · vs react-markdown · vs markdown-to-jsx · Renderer overview · Security model