markdown-to-jsx and react-markdown are both widely used libraries for converting Markdown content into React elements. They enable developers to safely render user-provided or static Markdown within React applications while offering extensibility through custom component overrides. Both parse Markdown strings and transform them into a tree of JSX elements, but they differ significantly in parsing strategy, customization model, security handling, and integration with the broader ecosystem.
Both markdown-to-jsx and react-markdown solve the same core problem: turning Markdown strings into React components. But under the hood, they take very different approaches — from how they parse text to how they let you customize output. Let’s break down what really matters when choosing between them.
markdown-to-jsx uses a regular expression-based parser. It scans the input string and matches patterns like # Heading or **bold** directly. This makes it extremely fast and keeps the bundle small, but it doesn’t fully comply with the CommonMark spec — edge cases (like nested emphasis or complex lists) may not render exactly as expected.
// markdown-to-jsx: Simple, direct parsing
import Markdown from 'markdown-to-jsx';
function App() {
return <Markdown>**Hello**, world!</Markdown>;
}
// Renders: <p><strong>Hello</strong>, world!</p>
react-markdown builds on the unified ecosystem (remark for Markdown parsing, rehype for HTML processing). It first converts Markdown into an Abstract Syntax Tree (AST), then walks that tree to generate React elements. This ensures high fidelity to the CommonMark spec and enables powerful plugin-based transformations.
// react-markdown: AST-based, spec-compliant
import ReactMarkdown from 'react-markdown';
function App() {
return <ReactMarkdown>**Hello**, world!</ReactMarkdown>;
}
// Also renders: <p><strong>Hello</strong>, world!</p>
// But handles edge cases more reliably
💡 If your Markdown comes from users or includes complex formatting (e.g., GitHub-flavored tables, footnotes),
react-markdown’s AST approach is safer and more predictable.
Both libraries let you replace default HTML tags with your own React components, but their APIs differ.
markdown-to-jsx uses an overrides prop that maps HTML tag names (like 'h1', 'a') to custom components:
// markdown-to-jsx: Override by tag name
import Markdown from 'markdown-to-jsx';
const MyLink = ({ href, children }) => (
<a href={href} className="custom-link">{children}</a>
);
function App() {
return (
<Markdown
overrides={{
a: { component: MyLink }
}}
>
[Visit site](https://example.com)
</Markdown>
);
}
react-markdown uses a components prop that maps Markdown node types (like 'link', 'heading') to components. These align with the underlying AST node types from mdast:
// react-markdown: Override by AST node type
import ReactMarkdown from 'react-markdown';
const MyLink = ({ href, children }) => (
<a href={href} className="custom-link">{children}</a>
);
function App() {
return (
<ReactMarkdown
components={{
link: MyLink
}}
>
[Visit site](https://example.com)
</ReactMarkdown>
);
}
⚠️ Note: In
react-markdown, the key is'link'(the Markdown AST node), not'a'(the HTML tag). This distinction matters when debugging.
Neither library executes arbitrary JavaScript by default, but their security models differ.
markdown-to-jsx does not sanitize HTML. If your Markdown contains raw HTML (e.g., <script>alert('xss')</script>), it will be rendered as-is unless you preprocess the input. You must handle sanitization yourself (e.g., with DOMPurify) before passing content to the component.
// markdown-to-jsx: Unsafe by default with HTML
<Markdown>
<img src="x" onerror="alert('xss')" />
</Markdown>
// ⚠️ This could execute scripts if not pre-sanitized!
react-markdown does not allow HTML by default. It escapes all HTML tags unless you explicitly enable them via the allowElement prop or use rehype-raw. For user-generated content, you typically pair it with rehype-sanitize:
// react-markdown: Safe by default
import ReactMarkdown from 'react-markdown';
import rehypeSanitize from 'rehype-sanitize';
function App() {
return (
<ReactMarkdown rehypePlugins={[rehypeSanitize]}>
<img src="x" onerror="alert('xss')" />
</ReactMarkdown>
);
}
// The script won't run — HTML is either escaped or sanitized
✅ For public-facing apps accepting Markdown from users,
react-markdown+rehype-sanitizeis the safer, more maintainable choice.
markdown-to-jsx is self-contained. It has no external dependencies and doesn’t support plugins. You can extend behavior only through overrides and preprocessing.
react-markdown is part of the unified ecosystem. You can plug in any remark plugin (e.g., remark-gfm for GitHub Flavored Markdown, remark-toc for tables of contents) or rehype plugin (e.g., rehype-highlight for syntax highlighting):
// react-markdown: Add GFM and syntax highlighting
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import remarkHighlight from 'remark-highlight';
function App() {
return (
<ReactMarkdown
remarkPlugins={[remarkGfm, remarkHighlight]}
>
```js
console.log('hello');
```
</ReactMarkdown>
);
}
markdown-to-jsx cannot do this natively — you’d need to preprocess the Markdown string before rendering.
markdown-to-jsx is significantly smaller (~5–7 KB minified) because it avoids heavy parser dependencies. Its regex engine is fast for typical Markdown.
react-markdown pulls in remark-parse, remark-rehype, and other unified packages, resulting in a larger footprint (~20–30 KB+). However, its performance is still acceptable for most use cases, and the trade-off is spec compliance and extensibility.
You’re building a company wiki where Markdown is written by engineers and stored in Git. Input is trusted, syntax is basic.
markdown-to-jsxUsers can format comments using Markdown, including links and code blocks. You must prevent XSS.
react-markdown + rehype-sanitize + remark-gfmYou need to style headings, links, and blockquotes consistently across hundreds of posts.
markdown-to-jsx if you want minimal overhead and simple overrides.react-markdown if you later need features like automatic heading IDs or table of contents.| Feature | markdown-to-jsx | react-markdown |
|---|---|---|
| Parser | Regex-based | AST-based (via remark) |
| Spec Compliance | Partial (CommonMark subset) | High (with plugins) |
| Customization | overrides by HTML tag | components by AST node type |
| HTML Handling | Renders raw HTML (unsafe by default) | Escapes HTML (safe by default) |
| Sanitization | Must be done externally | Built-in via rehype-sanitize |
| Plugins | ❌ Not supported | ✅ Full remark/rehype ecosystem |
| Bundle Size | Small (~5–7 KB) | Larger (~20–30 KB+) |
| Best For | Trusted content, simple Markdown | User-generated content, complex formatting |
markdown-to-jsx if you control the Markdown source, need speed and small size, and don’t require advanced syntax or plugins.react-markdown if you’re handling untrusted input, need GitHub-style Markdown, or plan to extend functionality with remark/rehype plugins.Both are mature, well-maintained libraries — the right choice depends entirely on your project’s trust model, feature needs, and performance constraints.
Choose markdown-to-jsx if you need a lightweight, fast parser with a simple override system based on HTML-like tag names. It’s ideal for projects where performance is critical, Markdown syntax is standard (CommonMark), and you want minimal dependencies. Its regex-based parser trades full spec compliance for speed and bundle size, making it well-suited for blogs, documentation sites, or internal tools where input is trusted or sanitized externally.
Choose react-markdown if you require strict adherence to the CommonMark specification, need to process complex or untrusted Markdown safely, or want deep integration with the unified/remark ecosystem. It’s better suited for applications that handle user-generated content, require syntax extensions via remark plugins, or demand predictable, spec-compliant rendering — especially when combined with rehype-sanitize for XSS protection.
markdown-to-jsx is a gfm+commonmark compliant markdown parser and compiler toolchain for JavaScript and TypeScript-based projects. It is extremely fast, capable of processing large documents fast enough for real-time interactivity.
Some special features of the library:
Arbitrary HTML is supported and parsed into the appropriate JSX representation
without dangerouslySetInnerHTML
Any HTML tags rendered by the compiler and/or <Markdown> component can be overridden to include additional props or even a different HTML representation entirely.
All GFM special syntaxes are supported, including tables, task lists, strikethrough, autolinks, tag filtering, and more.
Fenced code blocks with highlight.js support; see Syntax highlighting for instructions on setting up highlight.js.
Breaking Changes:
ast option removed: The ast: true option on compiler() has been removed. Use the new parser() function instead to access the AST directly./** v8 */ compiler('# Hello world', { ast: true })
/** v9 */ parser('# Hello world')
namedCodesToUnicode option removed: The namedCodesToUnicode option has been removed. All named HTML entities are now supported by default via the full entity list, so custom entity mappings are no longer needed./** v8 */ compiler('≤ symbol', { namedCodesToUnicode: { le: '\u2264' } })
/** v9 */ compiler('≤ symbol')
tagfilter enabled by default: Dangerous HTML tags (script, iframe, style, title, textarea, xmp, noembed, noframes, plaintext) are now escaped by default in both HTML string output and React JSX output. Previously these tags were rendered as JSX elements in React output./** v8 */ tags rendered as JSX elements
/** v9 */ tags escaped by default
compiler('<script>alert("xss")</script>') // <span><script></span>
/** Restore old behavior */
compiler('<script>alert("xss")</script>', { tagfilter: false })
New Features:
New parser function: Provides direct access to the parsed AST without rendering. This is the recommended way to get AST nodes.
New entry points: React-specific, HTML-specific, and markdown-specific entry points are now available for better tree-shaking and separation of concerns.
// React-specific usage
import Markdown, { compiler, parser } from 'markdown-to-jsx/react'
// HTML string output
import { compiler, astToHTML, parser } from 'markdown-to-jsx/html'
// Markdown string output (round-trip compilation)
import { compiler, astToMarkdown, parser } from 'markdown-to-jsx/markdown'
Migration Guide:
compiler(..., { ast: true }) with parser():/** v8 */ compiler(markdown, { ast: true })
/** v9 */ parser(markdown)
/react entry point (optional but recommended):/** Legacy */ import from 'markdown-to-jsx'
/** Recommended */ import from 'markdown-to-jsx/react'
namedCodesToUnicode option: All named HTML entities are now supported automatically, so you can remove any custom entity mappings./** v8 */ compiler('≤ symbol', { namedCodesToUnicode: { le: '\u2264' } })
/** v9 */ compiler('≤ symbol')
Note: The main entry point (markdown-to-jsx) continues to work for backward compatibility, but React code there is deprecated and will be removed in a future major release. Consider migrating to markdown-to-jsx/react for React-specific usage.
Breaking Changes:
ParserResult renamed to ASTNode - If you were using MarkdownToJSX.ParserResult in your code, update to MarkdownToJSX.ASTNode/** v7 */ MarkdownToJSX.ParserResult[]
/** v8+ */ MarkdownToJSX.ASTNode[]
RuleType enums consolidated into RuleType.textFormatted - If you were checking for RuleType.textBolded, RuleType.textEmphasized, RuleType.textMarked, or RuleType.textStrikethroughed, update to check for RuleType.textFormatted and inspect the node's boolean flags:/** v7 */ RuleType.textBolded
/** v8+ */ RuleType.textFormatted && node.bold
Install markdown-to-jsx with your favorite package manager.
npm i markdown-to-jsx
markdown-to-jsx exports a React component by default for easy JSX composition:
ES6-style usage*:
import Markdown from 'markdown-to-jsx'
import React from 'react'
import { render } from 'react-dom'
render(<Markdown># Hello world!</Markdown>, document.body)
/*
renders:
<h1>Hello world!</h1>
*/
* NOTE: JSX does not natively preserve newlines in multiline text. In general, writing markdown directly in JSX is discouraged and it's a better idea to keep your content in separate .md files and require them, perhaps using webpack's raw-loader.
markdown-to-jsx provides multiple entry points for different use cases:
The legacy default entry point exports everything, including the React compiler and component:
import Markdown, { compiler, parser } from 'markdown-to-jsx'
The React code in this entry point is deprecated and will be removed in a future major release, migrate to markdown-to-jsx/react.
For React-specific usage, import from the /react entry point:
import Markdown, { compiler, parser, astToJSX } from 'markdown-to-jsx/react'
const jsxElement = compiler('# Hello world')
function App() {
return <Markdown children="# Hello world" />
}
/** Or use parser + astToJSX */
const ast = parser('# Hello world')
const jsxElement2 = astToJSX(ast)
The Markdown component automatically detects whether it's running in a React Server Component (RSC) or client environment and adapts accordingly. No 'use client' directive is required.
Server Component (RSC) usage:
// Server Component - works automatically
import Markdown from 'markdown-to-jsx/react'
export default async function Page() {
const content = await fetchMarkdownContent()
return <Markdown>{content}</Markdown>
}
Client Component usage:
// Client Component - also works automatically
'use client'
import Markdown from 'markdown-to-jsx/react'
export function ClientMarkdown({ content }: { content: string }) {
return <Markdown>{content}</Markdown>
}
Notes:
MarkdownProvider and MarkdownContext are client-only and become no-ops in RSC environmentsFor React Native usage, import from the /native entry point:
import Markdown, { compiler, parser, astToNative } from 'markdown-to-jsx/native'
import { View, Text, StyleSheet, Linking } from 'react-native'
const nativeElement = compiler('# Hello world', {
styles: {
heading1: { fontSize: 32, fontWeight: 'bold' },
paragraph: { marginVertical: 8 },
link: { color: 'blue', textDecorationLine: 'underline' },
},
onLinkPress: url => {
Linking.openURL(url)
},
})
const markdown = `# Hello world
This is a [link](https://example.com) with **bold** and *italic* text.
`
function App() {
return (
<View>
<Markdown
children={markdown}
options={{
styles: StyleSheet.create({
heading1: { fontSize: 32, fontWeight: 'bold' },
paragraph: { marginVertical: 8 },
link: { color: 'blue', textDecorationLine: 'underline' },
}),
onLinkPress: url => {
Linking.openURL(url)
},
}}
/>
</View>
)
}
React Native-specific options:
onLinkPress?: (url: string, title?: string) => void - Custom handler for link presses (defaults to Linking.openURL)onLinkLongPress?: (url: string, title?: string) => void - Handler for link long pressesstyles?: Partial<Record<NativeStyleKey, StyleProp<ViewStyle | TextStyle | ImageStyle>>> - Style overrides for each element typewrapperProps?: ViewProps | TextProps - Props for the wrapper component (defaults to View for block, Text for inline)HTML Tag Mapping: HTML tags are automatically mapped to React Native components:
<img> → Image component<div>, <section>, <article>, <blockquote>, <ul>, <ol>, <li>, <table>, etc.) → View component<span>, <strong>, <em>, <a>, etc.) → Text component<pre>, <script>, <style>, <textarea>) → View componentNote: Links are underlined by default for better accessibility and discoverability. You can override this via the styles.link option.
For SolidJS usage, import from the /solid entry point:
import Markdown, {
compiler,
parser,
astToJSX,
MarkdownProvider,
} from 'markdown-to-jsx/solid'
import { createSignal } from 'solid-js'
// Static content
const solidElement = compiler('# Hello world')
function App() {
return <Markdown children="# Hello world" />
}
// Reactive content (automatically updates when content changes)
function ReactiveApp() {
const [content, setContent] = createSignal('# Hello world')
return <Markdown>{content}</Markdown>
}
// Or use parser + astToJSX
const ast = parser('# Hello world')
const solidElement2 = astToJSX(ast)
// Use context for default options
function AppWithContext() {
return (
<MarkdownProvider options={{ sanitizer: customSanitizer }}>
<Markdown># Content</Markdown>
</MarkdownProvider>
)
}
SolidJS-specific features:
Markdown component accepts signals/accessors for automatic updates when markdown content changesMarkdownProvider to provide default options and avoid prop drillingFor Vue.js 3 usage, import from the /vue entry point:
import Markdown, { compiler, parser, astToJSX } from 'markdown-to-jsx/vue'
import { h } from 'vue'
// Using compiler
const vnode = compiler('# Hello world')
// Using component
<Markdown children="# Hello world" />
// Or use parser + astToJSX
const ast = parser('# Hello world')
const vnode2 = astToJSX(ast)
Vue.js-specific features:
h() render function API@vue/babel-plugin-jsx or @vitejs/plugin-vue-jsxclass instead of className)For HTML string output (server-side rendering), import from the /html entry point:
import { compiler, html, parser } from 'markdown-to-jsx/html'
const htmlString = compiler('# Hello world')
/** Or use parser + html */
const ast = parser('# Hello world')
const htmlString2 = html(ast)
For markdown-to-markdown compilation (normalization and formatting), import from the /markdown entry point:
import { compiler, astToMarkdown, parser } from 'markdown-to-jsx/markdown'
const normalizedMarkdown = compiler('# Hello world\n\nExtra spaces!')
/** Or work with AST */
const ast = parser('# Hello world')
const normalizedMarkdown2 = astToMarkdown(ast)
| Option | Type | Default | Description |
|---|---|---|---|
createElement | function | - | Custom createElement behavior (React/React Native/SolidJS/Vue only). See createElement for details. |
disableAutoLink | boolean | false | Disable automatic conversion of bare URLs to anchor tags. |
disableParsingRawHTML | boolean | false | Disable parsing of raw HTML into JSX. |
enforceAtxHeadings | boolean | false | Require space between # and header text (GFM spec compliance). |
evalUnserializableExpressions | boolean | false | ⚠️ Eval unserializable props (DANGEROUS). See evalUnserializableExpressions for details. |
forceBlock | boolean | false | Force all content to be treated as block-level. |
forceInline | boolean | false | Force all content to be treated as inline. |
ignoreHTMLBlocks | boolean | false | Disable parsing of HTML blocks, treating them as plain text. |
forceWrapper | boolean | false | Force wrapper even with single child (React/React Native/Vue only). See forceWrapper for details. |
overrides | object | - | Override HTML tag rendering. See overrides for details. |
preserveFrontmatter | boolean | false | Include frontmatter in rendered output (as <pre> for HTML/JSX, included in markdown). Behavior varies by compiler type. |
renderRule | function | - | Custom rendering for AST rules. See renderRule for details. |
sanitizer | function | built-in | Custom URL sanitizer function. See sanitizer for details. |
slugify | function | built-in | Custom slug generation for heading IDs. See slugify for details. |
optimizeForStreaming | boolean | false | Suppress rendering of incomplete markdown syntax for streaming. See Streaming Markdown for details. |
tagfilter | boolean | true | Escape dangerous HTML tags (script, iframe, style, etc.) to prevent XSS. |
wrapper | string | component | null | 'div' | Wrapper element for multiple children (React/React Native/Vue only). See wrapper for details. |
wrapperProps | object | - | Props for wrapper element (React/React Native/Vue only). See wrapperProps for details. |
Sometimes, you might want to override the React.createElement default behavior to hook into the rendering process before the JSX gets rendered. This might be useful to add extra children or modify some props based on runtime conditions. The function mirrors the React.createElement function, so the params are type, [props], [...children]:
import Markdown from 'markdown-to-jsx'
import React from 'react'
import { render } from 'react-dom'
const md = `
# Hello world
`
render(
<Markdown
children={md}
options={{
createElement(type, props, children) {
return (
<div className="parent">
{React.createElement(type, props, children)}
</div>
)
},
}}
/>,
document.body
)
By default, the compiler does not wrap the rendered contents if there is only a single child. You can change this by setting forceWrapper to true. If the child is inline, it will not necessarily be wrapped in a span.
// Using `forceWrapper` with a single, inline child…
<Markdown options={{ wrapper: 'aside', forceWrapper: true }}>
Mumble, mumble…
</Markdown>
// renders
<aside>Mumble, mumble…</aside>
Override HTML tag rendering or render custom React components. Three use cases:
1. Remove tags: Return null to completely remove tags (beyond tagfilter escaping):
<Markdown options={{ overrides: { iframe: () => null } }}>
<iframe src="..."></iframe>
</Markdown>
2. Override HTML tags: Change component, props, or both:
const MyParagraph = ({ children, ...props }) => <div {...props}>{children}</div>
<Markdown options={{ overrides: { h1: { component: MyParagraph, props: { className: 'foo' } } } }}>
# Hello
</Markdown>
/** Simplified */ { overrides: { h1: MyParagraph } }
3. Render React components: Use custom components in markdown:
import DatePicker from './date-picker'
const md = `<DatePicker timezone="UTC+5" startTime={1514579720511} />`
<Markdown options={{ overrides: { DatePicker } }}>{md}</Markdown>
Important notes:
data={[1, 2, 3]} → parsed as [1, 2, 3]enabled={true} → parsed as trueonClick={() => ...} → kept as string for security (use renderRule for case-by-case handling, or see evalUnserializableExpressions)value={someVar} → kept as stringa (href, title), img (src, alt, title), input[type="checkbox"] (checked, readonly), ol (start), td/th (style)span for inline text, code for inline code, pre > code for code blocks⚠️ SECURITY WARNING: STRONGLY DISCOURAGED FOR USER INPUTS
When enabled, attempts to eval expressions in JSX props that cannot be serialized as JSON (functions, variables, complex expressions). This uses eval() which can execute arbitrary code.
By default (recommended), unserializable expressions are kept as strings for security:
import { parser } from 'markdown-to-jsx'
const ast = parser('<Button onClick={() => alert("hi")} />')
// ast[0].attrs.onClick === "() => alert(\"hi\")" (string, safe)
// Arrays and objects are automatically parsed (no eval needed):
const ast2 = parser('<Table data={[1, 2, 3]} />')
// ast2[0].attrs.data === [1, 2, 3] (parsed via JSON.parse)
ONLY enable this option when:
DO NOT enable this option when:
Example of the danger:
// User-submitted markdown with malicious code
const userMarkdown = '<Component onClick={() => fetch("/admin/delete-all")} />'
// ❌ DANGEROUS - function will be executable
parser(userMarkdown, { evalUnserializableExpressions: true })
// ✅ SAFE - function kept as string
parser(userMarkdown) // default behavior
Safe alternative: Use renderRule for case-by-case handling:
// Instead of eval'ing arbitrary expressions, handle them selectively in renderRule:
const handlers = {
handleClick: () => console.log('clicked'),
handleSubmit: () => console.log('submitted'),
}
compiler(markdown, {
renderRule(next, node) {
if (
node.type === RuleType.htmlBlock &&
typeof node.attrs?.onClick === 'string'
) {
// Option 1: Named handler lookup (safest)
const handler = handlers[node.attrs.onClick]
if (handler) {
return <button onClick={handler}>{/* ... */}</button>
}
// Option 2: Selective eval with allowlist (still risky)
if (
node.tag === 'TrustedComponent' &&
node.attrs.onClick.startsWith('() =>')
) {
try {
const fn = eval(`(${node.attrs.onClick})`)
return <button onClick={fn}>{/* ... */}</button>
} catch (e) {
// Handle error
}
}
}
return next()
},
})
This approach gives you full control over which expressions are evaluated and under what conditions.
When enabled, the parser will not attempt to parse HTML blocks. HTML syntax will be treated as plain text and rendered as-is.
<Markdown options={{ ignoreHTMLBlocks: true }}>
{'<div class="custom">This will be rendered as text</div>'}
</Markdown>
Supply your own rendering function that can selectively override how rules are rendered (note, this is different than options.overrides which operates at the HTML tag level and is more general). The renderRule function always executes before any other rendering code, giving you full control over how nodes are rendered, including normally-skipped nodes like ref, footnote, and frontmatter.
You can use this functionality to do pretty much anything with an established AST node; here's an example of selectively overriding the "codeBlock" rule to process LaTeX syntax using the @matejmazur/react-katex library:
import Markdown, { RuleType } from 'markdown-to-jsx'
import TeX from '@matejmazur/react-katex'
const exampleContent =
'Some important formula:\n\n```latex\nmathbb{N} = { a in mathbb{Z} : a > 0 }\n```\n'
function App() {
return (
<Markdown
children={exampleContent}
options={{
renderRule(next, node, renderChildren, state) {
if (node.type === RuleType.codeBlock && node.lang === 'latex') {
return (
<TeX as="div" key={state.key}>{String.raw`${node.text}`}</TeX>
)
}
return next()
},
}}
/>
)
}
Accessing parsed HTML content: For HTML blocks (like <script>, <style>, <pre>), renderRule can access the fully parsed AST in children:
<Markdown
options={{
renderRule(next, node, renderChildren) {
if (node.type === RuleType.htmlBlock && node.tag === 'script') {
// Access parsed children for custom rendering
const parsedContent = node.children || []
return <CustomScript content={parsedContent} />
}
return next()
},
}}
>
<script>Hello **world**</script>
</Markdown>
By default a lightweight URL sanitizer function is provided to avoid common attack vectors that might be placed into the href of an anchor tag, for example. The sanitizer receives the input, the HTML tag being targeted, and the attribute name. The original function is available as a library export called sanitizer.
This can be overridden and replaced with a custom sanitizer if desired via options.sanitizer:
// sanitizer in this situation would receive:
// ('javascript:alert("foo")', 'a', 'href')
<Markdown options={{ sanitizer: (value, tag, attribute) => value }}>
{`[foo](javascript:alert("foo"))`}
</Markdown>
// or
compiler('[foo](javascript:alert("foo"))', {
sanitizer: value => value,
})
By default, a lightweight deburring function is used to generate an HTML id from headings. You can override this by passing a function to options.slugify. This is helpful when you are using non-alphanumeric characters (e.g. Chinese or Japanese characters) in headings. For example:
<Markdown options={{ slugify: str => str }}># 中文</Markdown>
compiler('# 中文', { slugify: str => str })
The original function is available as a library export called slugify.
When there are multiple children to be rendered, the compiler will wrap the output in a div by default. You can override this default by setting the wrapper option to either a string (React Element) or a component.
const str = '# Heck Yes\n\nThis is great!'
<Markdown options={{ wrapper: 'article' }}>{str}</Markdown>
compiler(str, { wrapper: 'article' })
To get an array of children back without a wrapper, set wrapper to null. This is particularly useful when using compiler(…) directly.
compiler('One\n\nTwo\n\nThree', { wrapper: null })[
/** Returns */ ((<p>One</p>), (<p>Two</p>), (<p>Three</p>))
]
To render children at the same DOM level as <Markdown> with no HTML wrapper, set wrapper to React.Fragment. This will still wrap your children in a React node for the purposes of rendering, but the wrapper element won't show up in the DOM.
Props to apply to the wrapper element when wrapper is used.
<Markdown
options={{
wrapper: 'article',
wrapperProps: { className: 'post', 'data-testid': 'markdown-content' },
}}
>
# Hello World
</Markdown>
When using fenced code blocks with language annotation, that language will be added to the <code> element as class="lang-${language}". For best results, you can use options.overrides to provide an appropriate syntax highlighting integration like this one using highlight.js:
<!-- Add the following tags to your page <head> to automatically load hljs and styles: -->
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/obsidian.min.css"
/>
<script
crossorigin
src="https://unpkg.com/@highlightjs/cdn-assets@11.9.0/highlight.min.js"
></script>
import { Markdown, RuleType } from 'markdown-to-jsx'
const mdContainingFencedCodeBlock = '```js\nconsole.log("Hello world!");\n```\n'
function App() {
return (
<Markdown
children={mdContainingFencedCodeBlock}
options={{
overrides: {
code: SyntaxHighlightedCode,
},
}}
/>
)
}
function SyntaxHighlightedCode(props) {
const ref = React.useRef<HTMLElement | null>(null)
React.useEffect(() => {
if (ref.current && props.className?.includes('lang-') && window.hljs) {
window.hljs.highlightElement(ref.current)
// hljs won't reprocess the element unless this attribute is removed
ref.current.removeAttribute('data-highlighted')
}
}, [props.className, props.children])
return <code {...props} ref={ref} />
}
For Slack-style messaging with arbitrary shortcodes like :smile:, you can use options.renderRule to hook into the plain text rendering and adjust things to your liking, for example:
import Markdown, { RuleType } from 'markdown-to-jsx'
const shortcodeMap = {
smile: '🙂',
}
const detector = /(:[^:]+:)/g
const replaceEmoji = (text: string): React.ReactNode => {
return text.split(detector).map((part, index) => {
if (part.startsWith(':') && part.endsWith(':')) {
const shortcode = part.slice(1, -1)
return <span key={index}>{shortcodeMap[shortcode] || part}</span>
}
return part
})
}
function Example() {
return (
<Markdown
options={{
renderRule(next, node) {
if (node.type === RuleType.text && detector.test(node.text)) {
return replaceEmoji(node.text)
}
return next()
},
}}
>
{`On a beautiful summer day, all I want to do is :smile:.`}
</Markdown>
)
}
When you use options.renderRule, any React-renderable JSX may be returned including images and GIFs. Ensure you benchmark your solution as the text rule is one of the hottest paths in the system!
When rendering markdown content that arrives incrementally (e.g., from an AI/LLM API, WebSocket, or Server-Sent Events), you may notice raw markdown syntax briefly appearing before it renders properly. This happens because incomplete syntax like **bold text or <CustomComponent>partial content gets rendered as text before the closing delimiter arrives.
The optimizeForStreaming option solves this by detecting incomplete markdown structures and returning null (React) or empty string (HTML) until the content is complete:
import Markdown from 'markdown-to-jsx/react'
function StreamingMarkdown({ content }) {
return <Markdown options={{ optimizeForStreaming: true }}>{content}</Markdown>
}
LLM / AI chatbot integration:
A common pattern is rendering streamed responses from LLM APIs (OpenAI, Anthropic, etc.) where tokens arrive one at a time. Without optimizeForStreaming, users see distracting flashes of raw markdown syntax between each token. With it enabled, incomplete structures are suppressed until the closing delimiter arrives, producing a smooth reading experience:
import Markdown from 'markdown-to-jsx/react'
import { useState, useEffect } from 'react'
function ChatMessage({ stream }) {
const [content, setContent] = useState('')
useEffect(() => {
// Accumulate tokens from the LLM stream
stream.on('token', token => setContent(prev => prev + token))
}, [stream])
return <Markdown options={{ optimizeForStreaming: true }}>{content}</Markdown>
}
What it suppresses:
<div>content without </div>)<div attr="value without closing >)<!-- comment without -->)`code without closing backtick)**text or *text without closing)~~text without closing ~~)[text](url without closing ))What renders normally (content visible as it streams):
Everything will work just fine! Simply Alias react to preact/compat like you probably already are doing.
The Abstract Syntax Tree (AST) is a structured representation of parsed markdown. Each node in the AST has a type property that identifies its kind, and type-specific properties.
Important: The first node in the AST is typically a RuleType.refCollection node that contains all reference definitions found in the document, including footnotes (stored with keys prefixed with ^). This node is skipped during rendering but is useful for accessing reference data. Footnotes are automatically extracted from the refCollection and rendered in a <footer> element by both compiler() and astToJSX().
The AST consists of the following node types (use RuleType to check node types):
Block-level nodes:
RuleType.heading - Headings (# Heading)
{ type: RuleType.heading, level: 1, id: "heading", children: [...] }
RuleType.paragraph - Paragraphs
{ type: RuleType.paragraph, children: [...] }
RuleType.codeBlock - Fenced code blocks (```)
{ type: RuleType.codeBlock, lang: "javascript", text: "code content", attrs?: { "data-line": "1" } }
RuleType.blockQuote - Blockquotes (>)
{ type: RuleType.blockQuote, children: [...], alert?: "note" }
RuleType.orderedList / RuleType.unorderedList - Lists
{ type: RuleType.orderedList, items: [[...]], start?: 1 }
{ type: RuleType.unorderedList, items: [[...]] }
RuleType.table - Tables
{ type: RuleType.table, header: [...], cells: [[...]], align: [...] }
RuleType.htmlBlock - HTML blocks and JSX components
{ type: RuleType.htmlBlock, tag: "div", attrs?: Record<string, any>, children?: ASTNode[] }
Note (v9.1+): JSX components with blank lines between opening/closing tags now properly nest children instead of creating sibling nodes.
HTML Block Parsing (v9.2+): HTML blocks are always fully parsed into the children property. The renderRule callback can access the fully parsed AST in children for all HTML blocks.
Inline nodes:
RuleType.text - Plain text
{ type: RuleType.text, text: "Hello world" }
RuleType.textFormatted - Bold, italic, etc.
{ type: RuleType.textFormatted, tag: "strong", children: [...] }
RuleType.codeInline - Inline code (`)
{ type: RuleType.codeInline, text: "code" }
RuleType.link - Links
{ type: RuleType.link, target: "https://example.com", title?: "Link title", children: [...] }
RuleType.image - Images
{ type: RuleType.image, target: "image.png", alt?: "description", title?: "Image title" }
Other nodes:
RuleType.breakLine - Hard line breaks ( )RuleType.breakThematic - Horizontal rules (---)RuleType.gfmTask - GFM task list items (- [ ])
{ type: RuleType.gfmTask, completed: false }
RuleType.ref - Reference definition node (not rendered, stored in refCollection)RuleType.refCollection - Reference definitions collection (appears at AST root, includes footnotes with ^ prefix)
{ type: RuleType.refCollection, refs: { "label": { target: "url", title: "title" } } }
RuleType.footnote - Footnote definition node (not rendered, stored in refCollection)RuleType.footnoteReference - Footnote reference ([^identifier])
{ type: RuleType.footnoteReference, target: "#fn-identifier", text: "1" }
RuleType.frontmatter - YAML frontmatter blocks
{ type: RuleType.frontmatter, text: "---\ntitle: My Title\n---" }
RuleType.htmlComment - HTML comment nodes
{ type: RuleType.htmlComment, text: "comment text" }
RuleType.htmlSelfClosing - Self-closing HTML tags
{ type: RuleType.htmlSelfClosing, tag: "img", attrs?: { src: "image.png" } }
JSX Prop Parsing (v9.1+):
The parser intelligently parses JSX prop values:
JSON.parse(): rows={[["a", "b"]]} → attrs.rows = [["a", "b"]]onClick={() => ...} → attrs.onClick = "() => ..."enabled={true} → attrs.enabled = trueimport { parser, RuleType } from 'markdown-to-jsx'
const ast = parser(`# Hello World
This is a **paragraph** with [a link](https://example.com).
[linkref]: https://example.com
```javascript
console.log('code')
```
`)
// AST structure:
[
// Reference collection (first node, if references exist)
{
type: RuleType.refCollection,
refs: {
linkref: { target: 'https://example.com', title: undefined },
},
},
{
type: RuleType.heading,
level: 1,
id: 'hello-world',
children: [{ type: RuleType.text, text: 'Hello World' }],
},
{
type: RuleType.paragraph,
children: [
{ type: RuleType.text, text: 'This is a ' },
{
type: RuleType.textFormatted,
tag: 'strong',
children: [{ type: RuleType.text, text: 'paragraph' }],
},
{ type: RuleType.text, text: ' with ' },
{
type: RuleType.link,
target: 'https://example.com',
children: [{ type: RuleType.text, text: 'a link' }],
},
{ type: RuleType.text, text: '.' },
],
},
{
type: RuleType.codeBlock,
lang: 'javascript',
text: "console.log('code')",
},
]
Use the RuleType enum to identify AST nodes:
import { RuleType } from 'markdown-to-jsx'
if (node.type === RuleType.heading) {
const heading = node as MarkdownToJSX.HeadingNode
console.log(`Heading level ${heading.level}: ${heading.id}`)
}
When to use compiler vs parser vs <Markdown>:
<Markdown> when you need a simple React component that renders markdown to JSX.compiler when you need React JSX output from markdown (the component uses this internally).parser + astToJSX when you need the AST for custom processing before rendering to JSX, or just the AST itself.JSX prop parsing (v9.1+): Arrays and objects in JSX props are automatically parsed:
// In markdown:
<Table
columns={['Name', 'Age']}
data={[
['Alice', 30],
['Bob', 25],
]}
/>
// In your component (v9.1+):
const Table = ({ columns, data, ...props }) => {
// columns is already an array: ["Name", "Age"]
// data is already an array: [["Alice", 30], ["Bob", 25]]
// No JSON.parse needed!
}
// For backwards compatibility, check types:
const Table = ({ columns, data, ...props }) => {
const parsedColumns =
typeof columns === 'string' ? JSON.parse(columns) : columns
const parsedData = typeof data === 'string' ? JSON.parse(data) : data
}
Function props are kept as strings for security. Use renderRule for case-by-case handling, or see evalUnserializableExpressions for opt-in eval.
HTML indentation: Leading whitespace in HTML blocks is auto-trimmed based on the first line's indentation to avoid markdown syntax conflicts.
Code in HTML: Don't put code directly in HTML divs. Use fenced code blocks instead:
<div>
```js
var code = here();
```
</div>
See Github Releases.
Like this library? It's developed entirely on a volunteer basis; chip in a few bucks if you can via the Sponsor link!