react-markdown splits into three components based on where async plugins have to run
Markdown component for React
At a glance
- What is it?
- react-markdown is an MIT licensed React component that turns a markdown string into React elements from a syntax tree, with no dangerouslySetInnerHTML anywhere. It is CommonMark complete, puts GFM tables and footnotes behind remark-gfm, and ships ESM only, so the environment and module questions come up before the rendering ones do.
- Who is it for?
- react-markdown is the right default when markdown comes from users and you want React to diff a syntax tree rather than reparse a blob of HTML. It is the wrong tool for JSX inside markdown, which belongs to MDX, and for a CommonJS toolchain, because the package is ESM only.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 42 days ago.
- What is it written in?
- Mainly JavaScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on November 14, 2026, and from our analysis. They are not legal advice.
Star history
- remarkjs/react-markdown15,935 ★ · +47
| Repository | 2026-09-21 | 2026-11-14 |
|---|---|---|
| remarkjs/react-markdown | 15.9k | 15.9k |
47 Stars in the last 54 days, measured from the daily GitHub snapshot. Compare with other projects
Editorial analysis
MarkdownHooks renders nothing on the server, so the async plugin picks your component
Choosing the export is not a style decision, it is a decision about where your code runs. Three components ship. Markdown is synchronous. MarkdownAsync supports async plugins through async/await, and components that return promises are supported on the server; it returns a Promise to a React element. MarkdownHooks supports async plugins through hooks, using useEffect and useState, and returns a React node.
The constraint stated for MarkdownHooks is the one that bites. Hooks run on the client and do not immediately render something, and for async support on the server the README points to MarkdownAsync. So on a server-rendered page with an async plugin, MarkdownHooks gives you an empty first paint that fills in after hydration, while MarkdownAsync resolves before the response is sent.
The trap is picking the component by plugin rather than by environment. Choose MarkdownHooks on a server-rendered page because you saw useState in the docs, and your users get blank markdown followed by a flicker. One question settles it: does the first response need to contain the markdown?
The newest published release is 10.1.0 while the branch has moved for eighteen months
The tag list stops at 10.1.0, published on 2025-03-07, with 10.0.1 from 2025-03-03 and 10.0.0 from 2025-02-20 behind it. The default branch last took a push on 2026-10-03, and the repository is not archived.
That gap is the fact to plan around. Whatever npm install react-markdown resolves to is code from March 2025, while the commit history you would read to judge the project describes roughly eighteen further months of work that has never been cut into a release. A bug you read being fixed on main is not fixed in your node_modules.
So do not size your risk assessment from the activity signal. A recent last push tells you the project is alive and tells you nothing about which version you are running. When you evaluate an upgrade, compare your installed version against the tag rather than the tip of the branch, and expect changelog.md to lag well behind the commit log.
There is no CommonJS entry point to fall back to
The package is ESM only, and every documented install path reflects that. In Node.js version 16 or newer you install with npm:
npm install react-markdownIn Deno the import is a URL:
import Markdown from 'https://esm.sh/react-markdown@10'And in the browser it is a module script that pulls a bundle:
<script type="module">
import Markdown from 'https://esm.sh/react-markdown@10?bundle'
</script>What that costs is compatibility with older toolchains. There is no require path and no CommonJS build to fall back on, so a bundler or test runner still configured for CommonJS fails at the import rather than degrading gracefully. If you are adding this to an existing application, check that its transform pipeline understands ESM before you suspect the renderer, because the failure surfaces as a module resolution error and points nowhere near markdown.
Tables, footnotes and tasklists vanish until remark-gfm is in your tree
The compliance claim is precise: 100 percent CommonMark, and 100 percent GFM with a plugin. That second half is where people get caught. CommonMark has no tables, no footnotes, no strikethrough, no task lists and no automatic URLs, so a document using any of those renders as literal pipe and bracket characters until the plugin is installed.
The plugin is remark-gfm, and it is passed per component rather than configured globally:
import React from 'react'
import {createRoot} from 'react-dom/client'
import Markdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
const markdown = `Just a link: www.nasa.gov.`
createRoot(document.body).render(
<Markdown remarkPlugins={[remarkGfm]}>{markdown}</Markdown>
)The consequence for a CMS or documentation site is that authoring and rendering drift apart quietly. Writers produce markdown with tables because every other editor supports them, and the page renders a wall of pipes with no error logged anywhere. The fix is one import, and the check is to render one table and one footnote in a test rather than assuming your authoring tool's feature set matches your plugin list.
defaultUrlTransform keeps six protocols and silently discards the rest
Every href and src in rendered markdown passes through defaultUrlTransform, which follows how GitHub works. It allows http, https, irc, ircs, mailto and xmpp, plus URLs relative to the current protocol such as /something.
Two consequences follow from that list. The dangerous case is covered, because javascript: is not on the allowlist and never reaches the DOM as a link target. The irritating case is not: tel:, sms:, data: and custom application schemes are all discarded, so a support article whose entire purpose is a tap-to-call link renders as inert text with no warning that anything was dropped.
The function is exported, so you can pass your own urlTransform instead. That is the escape hatch, and it is also the moment you stop inheriting an allowlist curated against a known model of what a link can be. If you override it, write the tests the default gives you for free, including the scheme you are adding, because nothing warns you when the list and your expectations drift apart.
Every default you replace is a guarantee you take over
Safety here is a set of defaults rather than a property of the code. The component avoids dangerouslySetInnerHTML entirely, building a virtual DOM from a syntax tree so React replaces only what changed. Around that sit three overridable seams: Components maps tag names to your own components, AllowElement filters elements, and urlTransform decides which URLs survive.
AllowElement deserves close reading because of its signature. It receives the node from hast, the index of the element within its parent, and the parent node, and it returns a boolean or undefined.
The working rule is that each override moves a decision from the library to you. Swapping a component is safe while your component is safe. Extending urlTransform is safe while you keep the original allowlist and add to it. Write the negative test, the one that proves the thing is blocked, because what ships here is a set of positive guarantees rather than a filter watching every call.
The project scopes itself to beginners and names three jobs it declines
The positioning is stated rather than implied. This package focusses on making it easy for beginners to safely use markdown in React, and the reasoning offered against other React markdown options is that they often rely on dangerouslySetInnerHTML, have bugs in how they handle markdown, or do not let you swap elements for components.
From that scope it names alternatives for two cases. If you are already fluent in unified, it points you at react-remark or rehype-react used manually, since you can assemble the pipeline yourself. If you want JavaScript and JSX inside markdown files, that is MDX, and no plugin turns it into an option here.
Read those as boundaries rather than endorsements. A team with an existing unified pipeline is paying for an abstraction it does not need, and a team writing documentation with embedded components is in the wrong package entirely. Decide which of the three descriptions matches your project before adding the dependency, because the surface you will actually touch is much smaller than the export list suggests.
Editorial conclusion
react-markdown is the right default when markdown comes from users and you want React to diff a syntax tree rather than reparse a blob of HTML. It is the wrong tool for JSX inside markdown, which belongs to MDX, and for a CommonJS toolchain, because the package is ESM only. Before adopting it, decide which of the three components your rendering path needs, confirm remark-gfm is really in your tree, and pin a version knowing the newest published one is 10.1.0 from March 2025.
Frequently asked questions
how to install react markdown
In Node.js version 16 or newer, install with npm install react-markdown. The package is ESM only, so there is no CommonJS build to require. Deno and browser users import it from esm.sh instead, either as https://esm.sh/react-markdown@10 or with the ?bundle suffix appended.
how to use react-markdown
Pass the markdown string as children and render the component. Plugins go in remarkPlugins and your own replacements for HTML tags go in components. Pick the export by environment: Markdown is synchronous, MarkdownAsync supports async plugins on the server, and MarkdownHooks runs them on the client through useEffect and useState.
what is react markdown used for
Rendering markdown that comes from users or a content system as React elements, without dangerouslySetInnerHTML. The stated reasons to choose it over other React markdown options are that many rely on dangerouslySetInnerHTML, have bugs in how they handle markdown, or do not allow swapping elements for components.
is react markdown safe
It is safe by default because it never uses dangerouslySetInnerHTML, building a virtual DOM from a syntax tree instead. URL safety comes from defaultUrlTransform, which follows GitHub and permits only http, https, irc, ircs, mailto, xmpp and URLs relative to the current protocol. Overriding that function moves the decision to you.
react markdown vs mdx
They cover different needs. react-markdown renders CommonMark into React elements and points at MDX for the case where you want JavaScript and JSX inside markdown files. No plugin on react-markdown enables that, so choosing mdx is a decision about your authoring format rather than a rendering option.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/remarkjs-react-markdown)Star history chart for your README
[](https://hysenlabs.com/projects/remarkjs-react-markdown)Was this useful?