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 isn't a drop-in replacement, though. Three things differ, and two of them can fail silently, so this page goes through the evidence first and the reasons to switch after.

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 as text, and the javascript: destination gets emptied. Neither of those needed a plugin.

What matches​

docs/COMPATIBILITY.md is generated by the test run, so it stays in sync with the code. The same 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 uses the same algorithm and agrees on every probe URL, including javascript: and mailto:. skipHtml defaults to false in both packages.

The full matrix, case by case

The three differences​

className is ignored instead of throwing​

react-markdown 10 throws Unexpected className prop, remove it. This renderer doesn't have a className prop either, but it ignores it at runtime. The types reject it, so TypeScript code fails at build time. JavaScript code that still passes className just loses the wrapper without an error. Wrap the component instead.

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

MarkdownAsync and MarkdownHooks don't exist​

This renderer is synchronous. react-markdown 10 exports MarkdownAsync and MarkdownHooks for plugins that do asynchronous work. There's no equivalent export here, and async remark or rehype plugins won't run. The codemod leaves those imports alone and tells you why (codemod tests).

A dialect plugin can't 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, like remark-gfm, can't add tables to text that's 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 locally and don't send anything 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 does a dry run by default and only edits import statements. It keeps your alias and your quote style.

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

It won't guess. MarkdownAsync, MarkdownHooks and imports that reach into react-markdown/lib/* are left alone with an explanation. rehype-raw used without a sanitizer gets flagged instead of carried across, because that combination executes author HTML. Each of those cases has a test in tests/codemod.test.ts.

rmk-compare exits non-zero when it finds a difference, so you can gate a pull request 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​

These are server renders with renderToStaticMarkup, both sides parsing GFM, taking the median of the printed iteration count after five warm-up calls. The reference machine is an Apple M4 Max on Node 24.17, and the baseline is react-markdown@10.1.0. Ratios are kit divided by baseline, so anything below 1.00 is faster. You can reproduce them 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's still open. It comes from a fixed per-render cost and doesn't get worse with size. Building the unified processor and walking the content policy happens once per render no matter how big the document is. The fix is to cache the processor per resolved configuration. It's tracked in benchmarks/README.md and hasn't been done yet.

Re-rendering a document that was compiled once is faster at every size.

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

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

Install size​

This is how many 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. It's 1.3 KB gzipped more on CommonMark and 2.1 KB more with GFM. Both parse with micromark, so the extra size comes from the document contract and the policy layer. Size isn't a reason to switch.

When to stay on react-markdown​

I'd stay if any of these apply to you.

  • You use MarkdownAsync or MarkdownHooks, or an async remark or rehype plugin. There's no equivalent here.
  • Your smallest documents are on a hot path. The 1 KB case is 1.21x slower and the fix hasn't shipped.
  • Every byte counts and you render CommonMark only. react-markdown is 1.3 KB smaller gzipped.
  • You pass className from JavaScript and can't audit the call sites. TypeScript catches it, JavaScript doesn't.
  • Your current setup works and nothing on this page is a problem you have. Swapping renderers is some risk for no real gain if the one you have 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's already on screen. There are 59 tests for that 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 props match on 39 of 39 compared cases across 11 props, but className is ignored instead of throwing, MarkdownAsync and MarkdownHooks don't exist, and a dialect plugin such as remark-gfm can't 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 run npx rmk-migrate as a dry run, fix the className call sites it reports, and apply with --write. The codemod only changes import statements, and it won't touch MarkdownAsync, MarkdownHooks or 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 isn't a reason to switch.
Is React Markdown Kit faster than react-markdown?
Depends on the document. On the reference machine in benchmarks/README.md it's 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