Skip to main content

react-markdown alternative, with a codemod

@react-markdown-kit/renderer renders Markdown as React elements with the same prop names and the same meanings as react-markdown 10. It is not a drop-in replacement. Three things differ, and two of them can bite you silently, so this page lists the evidence before it lists the reasons.

npm install @react-markdown-kit/renderer
import Markdown from '@react-markdown-kit/renderer'

<Markdown>{content}</Markdown>
Rendered on this page by @react-markdown-kit/renderer
Markdown (edit me)
Rendered

Release notes

Ship safe output with no plugin list.

PackageGzipped
renderermeasured
editormeasured
  • raw HTML rendered as text
  • javascript: links emptied

Try it: <img src=x onerror="alert(1)"> and click.

The img tag and the script-shaped text stay text. The javascript: destination is emptied. Neither needed a plugin.

What matches

docs/COMPATIBILITY.md is generated by the test run, so it cannot drift from the code. Identical input goes through both packages and the normalized HTML is compared. Against react-markdown@10.1.0: 39 comparisons across 11 props, 39 produce identical markup (tests/compatibility.test.tsx, generated matrix).

PropCases matching
children4 of 4
components4 of 4
remarkPlugins4 of 4
rehypePlugins3 of 3
remarkRehypeOptions3 of 3
allowedElements4 of 4
disallowedElements3 of 3
allowElement3 of 3
unwrapDisallowed3 of 3
urlTransform4 of 4
skipHtml4 of 4

The default URL policy is the same algorithm and agrees on every probe URL, including javascript: and mailto:. skipHtml has the same default, false, in both packages.

The full matrix, case by case

The three differences

className is ignored, not rejected

react-markdown 10 throws Unexpected className prop, remove it. This renderer has no className prop either, but ignores it at runtime. TypeScript rejects it, so TypeScript code fails at build time. JavaScript code that still passes className loses the wrapper with no error. Wrap the component instead.

-<Markdown className="prose">{content}</Markdown>
+<div className="prose">
+ <Markdown>{content}</Markdown>
+</div>

MarkdownAsync and MarkdownHooks do not exist

This renderer is synchronous. react-markdown 10 exports MarkdownAsync and MarkdownHooks for plugins that do asynchronous work. There is no equivalent export here, and an async remark or rehype plugin will not run. The codemod leaves those imports alone and says why (codemod tests).

A dialect plugin cannot apply to a precompiled document

react-markdown only ever takes a string, so every plugin runs at parse time. This renderer also accepts a MarkdownDocument that was compiled earlier. A plugin that changes the dialect, such as remark-gfm, cannot add tables to text that has already been parsed. Tree-transforming plugins still run. Compile with the same extensions you render with, or pass the string (compiling a document).

The codemod

The renderer package ships two commands. Both run on your machine and send nothing anywhere.

npx rmk-compare 'content/**/*.md' --gfm # render your corpus through both, diff it
npx rmk-migrate 'src/**/*.tsx' # report what would change
npx rmk-migrate 'src/**/*.tsx' --write # apply it

rmk-migrate is a dry run by default and edits import statements only. It keeps your alias and your quote style.

-import ReactMarkdown from "react-markdown"
+import ReactMarkdown from "@react-markdown-kit/renderer"

It refuses to guess. MarkdownAsync, MarkdownHooks and imports that reach into react-markdown/lib/* are left untouched with an explanation, and rehype-raw used without a sanitizer is flagged rather than carried across, because that pair executes author HTML. Each of those refusals is a test in tests/codemod.test.ts.

rmk-compare exits non-zero on a difference, so a pull request can gate on it (tests/compare-runner.test.ts).

3 document(s) compared
matching 3 (100%)
differing 0
The full migration guide, step by step

Benchmarks, including the regression

Server rendering with renderToStaticMarkup, both sides parsing GFM, median of the printed iteration count after five warm-up calls. Reference machine: Apple M4 Max on Node 24.17. Baseline is react-markdown@10.1.0. Ratios are kit divided by baseline, so below 1.00 is faster. Reproduce with pnpm bench (benchmarks/run.mjs, method and results).

DocumentKit medianBaseline medianRatio
1 KB2.69 ms2.23 ms1.21x slower
10 KB16.22 ms16.25 ms1.00x
100 KB206.96 ms233.36 ms0.89x

The 1 KB row is a regression against react-markdown and it is open. It is fixed per-render cost, not a scaling problem: building the unified processor and walking the content policy happens once per render whatever the document size. The fix is to cache the processor per resolved configuration. It is tracked in benchmarks/README.md and not done.

Re-rendering a document that was compiled once is the path that is faster on any size.

Path10 KB median
From a source string16.80 ms
From a precompiled MarkdownDocument5.99 ms

Parsing is about two thirds of the work, and compileMarkdown does it once.

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. 1 KB is 1024 bytes.

One import, minified and gzipped
ImportVersionMinifiedGzipped
react-markdown, CommonMark10.1.0115.0 KB35.5 KB
@react-markdown-kit/renderer, CommonMark0.1.0118.5 KB36.8 KB
react-markdown + remark-gfm10.1.0152.6 KB46.4 KB
@react-markdown-kit/renderer + GFM preset0.1.0158.1 KB48.5 KB

The renderer is larger than react-markdown, not smaller: 1.3 KB gzipped more on CommonMark and 2.1 KB more with GFM. Both parse with micromark, so the difference is the document contract and the policy layer, not the parser. Size is not a reason to switch.

When to stay on react-markdown

Stay if any of these apply. They are the honest cases, not a disclaimer.

  • You use MarkdownAsync or MarkdownHooks, or an async remark or rehype plugin. There is no equivalent here.
  • Your smallest documents are on a hot path. The 1 KB case is 1.21x slower and the fix is not shipped.
  • Every byte counts and you render CommonMark only. react-markdown is 1.3 KB smaller gzipped.
  • You pass className from JavaScript and cannot audit the call sites. TypeScript catches it; JavaScript does not.
  • The setup works and nothing on this page is a problem you have. A renderer swap is risk with no reward if the current one is fine.

When to switch

  • You want raw HTML rendered as text and unsafe URLs emptied with no plugin list (security tests, security model).
  • You stream Markdown from a model and need every prefix of the document to render without throwing and without rewriting what is already on screen. That is 59 tests in streaming.test.tsx.
  • You render the same document more than once and want to parse it once (precompiled documents).
  • You want the editor, the Mermaid plugin, the slides plugin and the template plugin to read the same compiled document as the renderer (extensions).

FAQ

Is React Markdown Kit a drop-in replacement for react-markdown?
No, and docs/COMPATIBILITY.md says so in its first paragraph. The prop surface matches on 39 of 39 compared cases across 11 props, but className is ignored instead of throwing, MarkdownAsync and MarkdownHooks do not exist, and a dialect plugin such as remark-gfm cannot apply to a document that was already compiled without it.
How do I switch from react-markdown to React Markdown Kit?
Run npx rmk-compare on your own Markdown to see whether the output differs, then npx rmk-migrate as a dry run, fix the className call sites it reports, and apply with --write. The codemod changes import statements and nothing else, and it refuses to touch MarkdownAsync, MarkdownHooks and react-markdown internals.
Is React Markdown Kit smaller than react-markdown?
No. One import of the renderer is larger than one import of react-markdown, measured by scripts/compare-bundles.mjs and recorded in docs/data/bundle-sizes.json. Both parse with micromark, so size is not a reason to switch.
Is React Markdown Kit faster than react-markdown?
It depends on the document. On the reference machine in benchmarks/README.md it is 1.21x slower on a 1 KB document, at parity on 10 KB and 0.89x on 100 KB. A precompiled document re-renders 2.8x faster than parsing the same string again.

Next

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

Side by side with react-markdown · Side by side with markdown-to-jsx · Side by side with Streamdown · Migration guide · Renderer overview · Compatibility matrix