React component to render markdown safely with plugin support and custom component mapping
npx @tessl/cli install tessl/npm-react-markdown@10.1.0React Markdown is a React component library for safely rendering markdown content as React elements. It transforms markdown text into React JSX elements using the unified/remark ecosystem, offering comprehensive markdown parsing and rendering capabilities with support for CommonMark and GitHub Flavored Markdown through plugins. The library prioritizes security by avoiding dangerouslySetInnerHTML and XSS vulnerabilities, provides extensive customization through component mapping, and integrates seamlessly with the remark/rehype plugin ecosystem.
npm install react-markdownimport Markdown, { MarkdownAsync, MarkdownHooks, defaultUrlTransform } from "react-markdown";Individual imports:
import { default as Markdown, MarkdownAsync, MarkdownHooks, defaultUrlTransform } from "react-markdown";For CommonJS:
const Markdown = require("react-markdown").default;
const { MarkdownAsync, MarkdownHooks, defaultUrlTransform } = require("react-markdown");import React from "react";
import Markdown from "react-markdown";
function App() {
const markdown = `
# Hello World
This is **bold text** and this is *italic text*.
- List item 1
- List item 2
[Link to example](https://example.com)
`;
return (
<div>
<Markdown>{markdown}</Markdown>
</div>
);
}React Markdown is built around several key components:
Main component for rendering markdown synchronously. Best for most use cases where no async plugins are needed.
/**
* Synchronous React component to render markdown content
* @param options - Configuration options including markdown content and processing settings
* @returns React element containing the rendered markdown as React components
*/
function Markdown(options: Readonly<Options>): ReactElement;Component for server-side rendering with async plugin support.
/**
* Asynchronous React component for server-side rendering with async plugin support
* @param options - Configuration options including markdown content and async plugins
* @returns Promise that resolves to a React element with rendered markdown
*/
function MarkdownAsync(options: Readonly<Options>): Promise<ReactElement>;Component using React hooks for client-side async plugin support.
/**
* React component using hooks for client-side async plugin processing
* @param options - Extended configuration options with fallback content support
* @returns React node - either the rendered markdown or fallback content during processing
*/
function MarkdownHooks(options: Readonly<HooksOptions>): ReactNode;Usage Example:
import React from "react";
import { MarkdownHooks } from "react-markdown";
function AsyncMarkdown({ content }: { content: string }) {
return (
<MarkdownHooks
fallback={<div>Loading markdown...</div>}
remarkPlugins={[/* async plugins */]}
>
{content}
</MarkdownHooks>
);
}Default URL sanitization function to prevent XSS attacks.
/**
* Default URL sanitization function that prevents XSS attacks by validating URL protocols
* @param value - The URL string to sanitize and validate
* @returns The original URL if safe (relative or using allowed protocols), empty string if unsafe
*/
function defaultUrlTransform(value: string): string;Usage Example:
import Markdown, { defaultUrlTransform } from "react-markdown";
// Custom URL transformer
function customUrlTransform(url: string, key: string, node: Element): string {
// Apply default sanitization first
const safe = defaultUrlTransform(url);
// Add custom logic
if (key === 'href' && safe.startsWith('/')) {
return `https://mysite.com${safe}`;
}
return safe;
}
<Markdown urlTransform={customUrlTransform}>
[Internal link](/docs/api)
</Markdown>Replace default HTML elements with custom React components.
interface Components {
[Key in keyof JSX.IntrinsicElements]?:
| ComponentType<JSX.IntrinsicElements[Key] & ExtraProps>
| keyof JSX.IntrinsicElements;
}
interface ExtraProps {
/** Original HAST element (when passNode is enabled) */
node?: Element | undefined;
}Usage Example:
import React from "react";
import Markdown from "react-markdown";
const components = {
// Custom heading component
h1: ({ children, ...props }) => (
<h1 className="custom-heading" {...props}>
π― {children}
</h1>
),
// Custom code block
code: ({ inline, className, children, ...props }) => {
const match = /language-(\w+)/.exec(className || '');
return !inline && match ? (
<SyntaxHighlighter language={match[1]} {...props}>
{String(children).replace(/\n$/, '')}
</SyntaxHighlighter>
) : (
<code className={className} {...props}>
{children}
</code>
);
},
// Custom link component
a: ({ href, children }) => (
<a href={href} target="_blank" rel="noopener noreferrer">
{children} π
</a>
)
};
<Markdown components={components}>
# Hello World
Check out this [link](https://example.com)
\`\`\`javascript
console.log("Hello!");
\`\`\`
</Markdown>Control which HTML elements are allowed in the rendered output.
/**
* Callback function to filter elements during processing
* @param element - The HAST element to check for inclusion
* @param index - The index of the element within its parent's children array
* @param parent - The parent HAST element containing this element, or undefined for root elements
* @returns true to allow the element, false to remove it, null/undefined defaults to false
*/
type AllowElement = (
element: Readonly<Element>,
index: number,
parent: Readonly<Parents> | undefined
) => boolean | null | undefined;Usage Example:
import Markdown from "react-markdown";
// Only allow safe elements
<Markdown
allowedElements={['p', 'strong', 'em', 'ul', 'ol', 'li', 'h1', 'h2', 'h3']}
>
# Safe Markdown
This **bold text** is allowed, but <script>alert('xss')</script> is not.
</Markdown>
// Custom filtering logic
<Markdown
allowElement={(element, index, parent) => {
// Disallow images in list items
if (element.tagName === 'img' && parent?.tagName === 'li') {
return false;
}
return true;
}}
>
- This list item can have images: 
- This one will have the image filtered out
</Markdown>Extend markdown processing with remark and rehype plugins.
/**
* URL transformation callback for sanitizing and modifying URLs
* @param url - The original URL string to transform
* @param key - The HTML property name containing the URL (e.g., 'href', 'src', 'cite')
* @param node - The HAST element node containing the URL property
* @returns The transformed URL string, or null/undefined to remove the URL
*/
type UrlTransform = (
url: string,
key: string,
node: Readonly<Element>
) => string | null | undefined;Usage Example:
import Markdown from "react-markdown";
import remarkGfm from "remark-gfm";
import remarkToc from "remark-toc";
import remarkMath from "remark-math";
import rehypeRaw from "rehype-raw";
import rehypeHighlight from "rehype-highlight";
import rehypeKatex from "rehype-katex";
import rehypeSlug from "rehype-slug";
// Basic plugin usage
<Markdown
remarkPlugins={[
remarkGfm, // GitHub Flavored Markdown
remarkToc // Table of contents
]}
rehypePlugins={[
rehypeRaw, // Allow raw HTML
rehypeHighlight // Syntax highlighting
]}
remarkRehypeOptions={{
allowDangerousHtml: true
}}
>
# My Document
## Table of Contents
| Feature | Supported |
|---------|-----------|
| Tables | β
|
| Lists | β
|
~~Strikethrough~~ text is supported with remarkGfm.
```javascript
// This will be syntax highlighted
console.log("Hello World!");// Advanced plugin configuration with options <Markdown remarkPlugins={[ remarkGfm, [remarkToc, { heading: "contents", maxDepth: 3 }], [remarkMath, { singleDollarTextMath: false }] ]} rehypePlugins={[ rehypeSlug, [rehypeHighlight, { detect: true, ignoreMissing: true, subset: ['javascript', 'typescript', 'css'] }], [rehypeKatex, { strict: false, trust: false, macros: { "\RR": "\mathbb{R}", "\NN": "\mathbb{N}" } }] ]} remarkRehypeOptions={{ allowDangerousHtml: true, clobberPrefix: 'user-content-', footnoteLabel: 'Footnotes', footnoteLabelTagName: 'h2' }}
Inline math: $E = mc^2$ and display math:
$$ \int_{-\infty}^{\infty} e^{-x^2} dx = \sqrt{\pi} $$
// TypeScript code with syntax highlighting
interface User {
id: number;
name: string;
}// Custom plugin example function customRemarkPlugin() { return (tree, file) => { // Transform AST nodes visit(tree, 'text', (node) => { node.value = node.value.replace(/TODO:/g, 'π TODO:'); }); }; }
<Markdown remarkPlugins={[ customRemarkPlugin, [remarkGfm, { singleTilde: false }] ]}
TODO: This will be prefixed with an emoji
This strikethrough requires double tildes
</Markdown>
## External Type Dependencies
```typescript { .api }
// From 'hast' package
interface Element {
type: 'element';
tagName: string;
properties: Properties;
children: Array<Element | Text | Comment>;
position?: Position;
}
interface Properties {
[key: string]: boolean | number | string | null | undefined | Array<string | number>;
}
interface Text {
type: 'text';
value: string;
position?: Position;
}
interface Comment {
type: 'comment';
value: string;
position?: Position;
}
interface Parents {
type: string;
children: Array<Element | Text | Comment>;
}
interface Position {
start: Point;
end: Point;
}
interface Point {
line: number;
column: number;
offset?: number;
}
// From 'unified' package
type PluggableList = Array<Pluggable>;
type Pluggable = Plugin | Preset | [Plugin, ...Parameters] | [Preset, ...Parameters];
type Plugin = (this: Processor, ...parameters: any[]) => Transformer | void;
type Preset = { plugins: PluggableList; settings?: Settings };
type Transformer = (tree: any, file: any, next?: Function) => any;
type Parameters = any[];
type Settings = Record<string, any>;
interface Processor {
use(...args: any[]): Processor;
parse(document: string | Buffer): any;
run(tree: any, file?: any, callback?: Function): any;
runSync(tree: any, file?: any): any;
}
// From 'remark-rehype' package
interface RemarkRehypeOptions {
allowDangerousHtml?: boolean;
passThrough?: Array<string>;
handlers?: Record<string, Function>;
unknownHandler?: Function;
clobberPrefix?: string;
footnoteLabel?: string;
footnoteLabelTagName?: string;
footnoteLabelProperties?: Properties;
footnoteBackLabel?: string;
}
// From 'vfile' package
interface VFile {
value: string | Buffer;
path?: string;
basename?: string;
stem?: string;
extname?: string;
dirname?: string;
history: Array<string>;
messages: Array<VFileMessage>;
data: Record<string, any>;
}
interface VFileMessage {
reason: string;
line?: number;
column?: number;
position?: Position;
ruleId?: string;
source?: string;
fatal?: boolean;
}interface Options {
/**
* Markdown content to render as a string
* @default undefined
*/
children?: string | null | undefined;
/**
* Custom callback function to programmatically filter elements during processing
* Called after allowedElements/disallowedElements filtering
* @default undefined
*/
allowElement?: AllowElement | null | undefined;
/**
* Array of HTML tag names to allow in output (whitelist approach)
* Cannot be used together with disallowedElements
* @default undefined (allows all elements)
*/
allowedElements?: ReadonlyArray<string> | null | undefined;
/**
* Array of HTML tag names to remove from output (blacklist approach)
* Cannot be used together with allowedElements
* @default []
*/
disallowedElements?: ReadonlyArray<string> | null | undefined;
/**
* Object mapping HTML tag names to custom React components
* @default undefined
*/
components?: Components | null | undefined;
/**
* Array of remark plugins to process markdown before converting to HTML
* Plugins can be functions or [plugin, options] tuples
* @default []
*/
remarkPlugins?: PluggableList | null | undefined;
/**
* Array of rehype plugins to process HTML after markdown conversion
* Plugins can be functions or [plugin, options] tuples
* @default []
*/
rehypePlugins?: PluggableList | null | undefined;
/**
* Configuration options passed to remark-rehype for markdown to HTML conversion
* @default { allowDangerousHtml: true }
*/
remarkRehypeOptions?: Readonly<RemarkRehypeOptions> | null | undefined;
/**
* Whether to completely ignore HTML in markdown input
* When true, HTML tags are treated as plain text
* @default false
*/
skipHtml?: boolean | null | undefined;
/**
* Whether to extract children from disallowed elements instead of removing entirely
* When false, disallowed elements and their children are removed
* When true, only the element wrapper is removed, children are kept
* @default false
*/
unwrapDisallowed?: boolean | null | undefined;
/**
* Function to transform/sanitize URLs in href, src, and other URL attributes
* Receives the URL, attribute name, and containing element
* @default defaultUrlTransform
*/
urlTransform?: UrlTransform | null | undefined;
}
interface HooksOptions extends Options {
/**
* React content to display while markdown is being processed asynchronously
* Only used by MarkdownHooks component for client-side async processing
* @default undefined
*/
fallback?: ReactNode | null | undefined;
}React Markdown prioritizes security with multiple built-in protections:
defaultUrlTransform prevents XSS via malicious URLsSecurity Example:
import Markdown, { defaultUrlTransform } from "react-markdown";
// Highly secure configuration
<Markdown
// Only allow safe elements
allowedElements={[
'p', 'strong', 'em', 'ul', 'ol', 'li',
'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
'blockquote', 'code', 'pre'
]}
// Skip any raw HTML
skipHtml={true}
// Use default URL sanitization
urlTransform={defaultUrlTransform}
>
{untrustedMarkdown}
</Markdown>React Markdown handles various error conditions gracefully:
Error Handling Example:
import React from "react";
import { MarkdownHooks } from "react-markdown";
function SafeMarkdown({ content }: { content: string }) {
return (
<ErrorBoundary fallback={<div>Failed to render markdown</div>}>
<MarkdownHooks
fallback={<div>Loading...</div>}
remarkPlugins={[/* potentially failing async plugins */]}
>
{content}
</MarkdownHooks>
</ErrorBoundary>
);
}