markdown-to-jsx
markdown-to-jsx 9 is a CommonMark and GitHub Flavored Markdown parser plus a family of renderers. Separate entry points turn Markdown into React elements, React Native components, SolidJS nodes, Vue nodes, HTML strings, normalized Markdown, or a typed abstract syntax tree. The React component is the familiar face, but the package now covers much more than JSX. It parses raw HTML without dangerouslySetInnerHTML, lets you replace tags with application components, supports GFM tables and task lists, and adds URL and raw-HTML filtering. That flexibility is useful for documentation and controlled rich text, but it also creates a larger security and configuration surface than a plain Markdown renderer.
A capable choice when custom component overrides or one parser across several UI frameworks is the point. For ordinary React Markdown with an established plugin pipeline, react-markdown is easier to compose and audit.
Use it if
- You need GFM Markdown rendered into React, React Native, Solid, Vue, or an HTML string from one parser
- You want custom components inside Markdown through tag overrides, without injecting an HTML string into the DOM
- You need access to a typed AST for transforms before rendering or want to normalize Markdown back to Markdown
- You render incrementally arriving Markdown and want incomplete emphasis, links, tables, and tags suppressed until complete
- You only need a small standards-level parser: the React-oriented bundle is 27.3 KB gzipped, while lower-level parsers such as micromark give you a narrower base
- You want a unified or remark plugin pipeline: this package has overrides and renderRule, not the broad syntax-tree plugin ecosystem used by react-markdown
- Your app is stuck on Node older than 18: version 9.10.2 declares Node 18 as its minimum runtime
- You cannot budget for major-version migrations: v9 removed compiler's ast option, changed the preferred React import, removed namedCodesToUnicode, and enabled tag filtering by default
- You plan to enable evalUnserializableExpressions for user content: the README says this uses eval and can execute arbitrary code, so that configuration is unacceptable for untrusted Markdown
Setup reality
Install markdown-to-jsx and import from the renderer-specific subpath you actually use. In version 9, React code should come from markdown-to-jsx/react; the root entry still exports it for compatibility but is deprecated and scheduled for removal in a future major. The package has no runtime dependencies. React, Vue, and Solid are optional peers, so install only the framework behind your chosen entry point. Both ESM and CommonJS exports and bundled declarations are present, and Node 18 or newer is required. Raw HTML parsing is on by default. Version 9 filters dangerous tag names and strips event handlers and dangerous URL attributes, while a separate sanitizer handles link schemes. Those defenses are useful, but they do not turn arbitrary custom component props into a harmless capability system: overrides decide which application components untrusted authors can instantiate. Never enable evalUnserializableExpressions for user-controlled text because it evaluates code. data:image/svg+xml remains a documented caveat when opened as a top-level navigation. JSX source text does not preserve Markdown newlines reliably, so keep substantial content in strings or .md files rather than indenting it inside JSX. Multiple block nodes get a div wrapper unless you choose another wrapper, React.Fragment, or null when using compiler. Code fences only get language classes; syntax highlighting requires your own component or library. For streaming text, enable optimizeForStreaming or users may briefly see unfinished Markdown delimiters. If moving from v8, replace compiler(source, { ast: true }) with parser(source), review the new default tagfilter, and migrate React imports to the /react entry point.
Patterns
Render Markdown in Reactrender-react
import Markdown from 'markdown-to-jsx/react';
export function Article({ source }: { source: string }) {
return <Markdown>{source}</Markdown>;
}Use the /react entry point in version 9. The root React export still works but is deprecated.
Render blocks inside a semantic wrapperset-wrapper
import Markdown from 'markdown-to-jsx/react';
<Markdown options={{
wrapper: 'article',
wrapperProps: { className: 'prose', 'data-testid': 'article' },
}}>
{source}
</Markdown>Multiple top-level nodes use a div by default. wrapperProps only applies when a wrapper is actually rendered.
Replace Markdown tags with React componentsoverride-elements
import Markdown from 'markdown-to-jsx/react';
import { AppLink } from './AppLink';
<Markdown options={{
overrides: {
a: { component: AppLink, props: { trackingSource: 'docs' } },
h1: { props: { className: 'page-title' } },
},
}}>
{source}
</Markdown>An override applies to both Markdown-generated tags and matching raw HTML tags. Audit which custom components untrusted authors can reach.
Drop an unwanted HTML elementremove-html-tag
import Markdown from 'markdown-to-jsx/react';
const Remove = () => null;
<Markdown options={{
overrides: { iframe: Remove },
}}>
{source}
</Markdown>tagfilter escapes dangerous tags by default; an override returning null removes the element instead of showing its inert source.
Treat raw HTML as textdisable-raw-html
import Markdown from 'markdown-to-jsx/react';
<Markdown options={{ disableParsingRawHTML: true }}>
{userContent}
</Markdown>This narrows the feature surface for user content. Keep tagfilter enabled and do not enable expression evaluation.
Allow only selected URL schemescustomize-url-sanitizer
import Markdown, { sanitizer } from 'markdown-to-jsx/react';
const safeUrl = (value: string, tag: string, attribute: string) => {
if (value.startsWith('mailto:') || value.startsWith('https://')) return value;
return sanitizer(value, tag, attribute);
};
<Markdown options={{ sanitizer: safeUrl }}>{source}</Markdown>The URL sanitizer and raw HTML attribute filtering are separate defenses. Returning input unchanged disables scheme protection.
Parse Markdown into an ASTparse-ast
import { parser, RuleType } from 'markdown-to-jsx/react';
const ast = parser(source);
const headings = ast.filter((node) => node.type === RuleType.heading);
console.log(headings.map((node) => ({ level: node.level, id: node.id })));In version 9, use parser(); compiler(source, { ast: true }) was removed. The first node is often a reference collection.
Convert Markdown to an HTML stringcompile-html
import { compiler } from 'markdown-to-jsx/html';
const html = compiler('# Release notes
- Fixed login
- Added export');This uses the dedicated HTML entry point and returns a string. Review how you later insert that string into a page.
Parse and write Markdown againnormalize-markdown
import { compiler } from 'markdown-to-jsx/markdown';
const normalized = compiler(source);Normalization can change formatting while preserving structure, so show a diff before overwriting author-owned files.
Hide incomplete syntax during streamingrender-streaming-markdown
import Markdown from 'markdown-to-jsx/react';
export function StreamingMessage({ content }: { content: string }) {
return (
<Markdown options={{ optimizeForStreaming: true }}>
{content}
</Markdown>
);
}The option suppresses unfinished links, emphasis, tables, and HTML. Fenced code content remains visible while it arrives.
Generate heading IDs with a custom sluggercustomize-heading-ids
import Markdown from 'markdown-to-jsx/react';
const slugify = (text: string) => text
.normalize('NFKD')
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, '');
<Markdown options={{ slugify }}>{source}</Markdown>The library still adds numeric suffixes when your slug function returns the same ID for multiple headings.
Replace fenced code blocks with a highlighterrender-code-blocks
import Markdown, { RuleType } from 'markdown-to-jsx/react';
<Markdown options={{
renderRule(next, node, _children, state) {
if (node.type === RuleType.codeBlock) {
return <CodeBlock key={state.key} code={node.text} language={node.lang} />;
}
return next();
},
}}>
{source}
</Markdown>Code fences only receive language classes by default. renderRule exposes node.lang and node.text for a real highlighter.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| react-markdown | npm | Use it for React rendering built around unified, remark, and rehype plugins |
| marked | npm | Use it when you primarily need fast Markdown-to-HTML conversion and will own sanitization |
| micromark | npm | Use it as a lower-level CommonMark parser when a small standards-focused core matters more than components |