React component to render markdown safely with plugin support and custom component mapping
npx @tessl/cli install tessl/npm-react-markdown@10.1.00
# React Markdown
1
2
React 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.
3
4
## Package Information
5
6
- **Package Name**: react-markdown
7
- **Package Type**: npm
8
- **Language**: JavaScript/TypeScript (ES Modules)
9
- **Installation**: `npm install react-markdown`
10
11
## Core Imports
12
13
```typescript
14
import Markdown, { MarkdownAsync, MarkdownHooks, defaultUrlTransform } from "react-markdown";
15
```
16
17
Individual imports:
18
19
```typescript
20
import { default as Markdown, MarkdownAsync, MarkdownHooks, defaultUrlTransform } from "react-markdown";
21
```
22
23
For CommonJS:
24
25
```javascript
26
const Markdown = require("react-markdown").default;
27
const { MarkdownAsync, MarkdownHooks, defaultUrlTransform } = require("react-markdown");
28
```
29
30
## Basic Usage
31
32
```typescript
33
import React from "react";
34
import Markdown from "react-markdown";
35
36
function App() {
37
const markdown = `
38
# Hello World
39
40
This is **bold text** and this is *italic text*.
41
42
- List item 1
43
- List item 2
44
45
[Link to example](https://example.com)
46
`;
47
48
return (
49
<div>
50
<Markdown>{markdown}</Markdown>
51
</div>
52
);
53
}
54
```
55
56
## Architecture
57
58
React Markdown is built around several key components:
59
60
- **Processing Pipeline**: Uses unified ecosystem (remark β rehype β React) for reliable markdown parsing
61
- **Security-First**: No dangerouslySetInnerHTML usage, built-in URL sanitization, element filtering
62
- **Component Mapping**: Replace any HTML element with custom React components
63
- **Plugin System**: Extensive plugin support for both markdown (remark) and HTML (rehype) processing
64
- **Multiple Rendering Modes**: Synchronous, asynchronous server-side, and client-side hooks support
65
66
## Capabilities
67
68
### Synchronous Markdown Rendering
69
70
Main component for rendering markdown synchronously. Best for most use cases where no async plugins are needed.
71
72
```typescript { .api }
73
/**
74
* Synchronous React component to render markdown content
75
* @param options - Configuration options including markdown content and processing settings
76
* @returns React element containing the rendered markdown as React components
77
*/
78
function Markdown(options: Readonly<Options>): ReactElement;
79
```
80
81
### Asynchronous Server-Side Rendering
82
83
Component for server-side rendering with async plugin support.
84
85
```typescript { .api }
86
/**
87
* Asynchronous React component for server-side rendering with async plugin support
88
* @param options - Configuration options including markdown content and async plugins
89
* @returns Promise that resolves to a React element with rendered markdown
90
*/
91
function MarkdownAsync(options: Readonly<Options>): Promise<ReactElement>;
92
```
93
94
### Client-Side Hooks Rendering
95
96
Component using React hooks for client-side async plugin support.
97
98
```typescript { .api }
99
/**
100
* React component using hooks for client-side async plugin processing
101
* @param options - Extended configuration options with fallback content support
102
* @returns React node - either the rendered markdown or fallback content during processing
103
*/
104
function MarkdownHooks(options: Readonly<HooksOptions>): ReactNode;
105
```
106
107
**Usage Example:**
108
109
```typescript
110
import React from "react";
111
import { MarkdownHooks } from "react-markdown";
112
113
function AsyncMarkdown({ content }: { content: string }) {
114
return (
115
<MarkdownHooks
116
fallback={<div>Loading markdown...</div>}
117
remarkPlugins={[/* async plugins */]}
118
>
119
{content}
120
</MarkdownHooks>
121
);
122
}
123
```
124
125
### URL Transformation
126
127
Default URL sanitization function to prevent XSS attacks.
128
129
```typescript { .api }
130
/**
131
* Default URL sanitization function that prevents XSS attacks by validating URL protocols
132
* @param value - The URL string to sanitize and validate
133
* @returns The original URL if safe (relative or using allowed protocols), empty string if unsafe
134
*/
135
function defaultUrlTransform(value: string): string;
136
```
137
138
**Usage Example:**
139
140
```typescript
141
import Markdown, { defaultUrlTransform } from "react-markdown";
142
143
// Custom URL transformer
144
function customUrlTransform(url: string, key: string, node: Element): string {
145
// Apply default sanitization first
146
const safe = defaultUrlTransform(url);
147
148
// Add custom logic
149
if (key === 'href' && safe.startsWith('/')) {
150
return `https://mysite.com${safe}`;
151
}
152
153
return safe;
154
}
155
156
<Markdown urlTransform={customUrlTransform}>
157
[Internal link](/docs/api)
158
</Markdown>
159
```
160
161
### Component Customization
162
163
Replace default HTML elements with custom React components.
164
165
```typescript { .api }
166
interface Components {
167
[Key in keyof JSX.IntrinsicElements]?:
168
| ComponentType<JSX.IntrinsicElements[Key] & ExtraProps>
169
| keyof JSX.IntrinsicElements;
170
}
171
172
interface ExtraProps {
173
/** Original HAST element (when passNode is enabled) */
174
node?: Element | undefined;
175
}
176
```
177
178
**Usage Example:**
179
180
```typescript
181
import React from "react";
182
import Markdown from "react-markdown";
183
184
const components = {
185
// Custom heading component
186
h1: ({ children, ...props }) => (
187
<h1 className="custom-heading" {...props}>
188
π― {children}
189
</h1>
190
),
191
192
// Custom code block
193
code: ({ inline, className, children, ...props }) => {
194
const match = /language-(\w+)/.exec(className || '');
195
return !inline && match ? (
196
<SyntaxHighlighter language={match[1]} {...props}>
197
{String(children).replace(/\n$/, '')}
198
</SyntaxHighlighter>
199
) : (
200
<code className={className} {...props}>
201
{children}
202
</code>
203
);
204
},
205
206
// Custom link component
207
a: ({ href, children }) => (
208
<a href={href} target="_blank" rel="noopener noreferrer">
209
{children} π
210
</a>
211
)
212
};
213
214
<Markdown components={components}>
215
# Hello World
216
217
Check out this [link](https://example.com)
218
219
\`\`\`javascript
220
console.log("Hello!");
221
\`\`\`
222
</Markdown>
223
```
224
225
### Element Filtering
226
227
Control which HTML elements are allowed in the rendered output.
228
229
```typescript { .api }
230
/**
231
* Callback function to filter elements during processing
232
* @param element - The HAST element to check for inclusion
233
* @param index - The index of the element within its parent's children array
234
* @param parent - The parent HAST element containing this element, or undefined for root elements
235
* @returns true to allow the element, false to remove it, null/undefined defaults to false
236
*/
237
type AllowElement = (
238
element: Readonly<Element>,
239
index: number,
240
parent: Readonly<Parents> | undefined
241
) => boolean | null | undefined;
242
```
243
244
**Usage Example:**
245
246
```typescript
247
import Markdown from "react-markdown";
248
249
// Only allow safe elements
250
<Markdown
251
allowedElements={['p', 'strong', 'em', 'ul', 'ol', 'li', 'h1', 'h2', 'h3']}
252
>
253
# Safe Markdown
254
255
This **bold text** is allowed, but <script>alert('xss')</script> is not.
256
</Markdown>
257
258
// Custom filtering logic
259
<Markdown
260
allowElement={(element, index, parent) => {
261
// Disallow images in list items
262
if (element.tagName === 'img' && parent?.tagName === 'li') {
263
return false;
264
}
265
return true;
266
}}
267
>
268
- This list item can have images: 
269
- This one will have the image filtered out
270
</Markdown>
271
```
272
273
### Plugin System
274
275
Extend markdown processing with remark and rehype plugins.
276
277
```typescript { .api }
278
/**
279
* URL transformation callback for sanitizing and modifying URLs
280
* @param url - The original URL string to transform
281
* @param key - The HTML property name containing the URL (e.g., 'href', 'src', 'cite')
282
* @param node - The HAST element node containing the URL property
283
* @returns The transformed URL string, or null/undefined to remove the URL
284
*/
285
type UrlTransform = (
286
url: string,
287
key: string,
288
node: Readonly<Element>
289
) => string | null | undefined;
290
```
291
292
**Usage Example:**
293
294
```typescript
295
import Markdown from "react-markdown";
296
import remarkGfm from "remark-gfm";
297
import remarkToc from "remark-toc";
298
import remarkMath from "remark-math";
299
import rehypeRaw from "rehype-raw";
300
import rehypeHighlight from "rehype-highlight";
301
import rehypeKatex from "rehype-katex";
302
import rehypeSlug from "rehype-slug";
303
304
// Basic plugin usage
305
<Markdown
306
remarkPlugins={[
307
remarkGfm, // GitHub Flavored Markdown
308
remarkToc // Table of contents
309
]}
310
rehypePlugins={[
311
rehypeRaw, // Allow raw HTML
312
rehypeHighlight // Syntax highlighting
313
]}
314
remarkRehypeOptions={{
315
allowDangerousHtml: true
316
}}
317
>
318
# My Document
319
320
## Table of Contents
321
322
| Feature | Supported |
323
|---------|-----------|
324
| Tables | β |
325
| Lists | β |
326
327
~~Strikethrough~~ text is supported with remarkGfm.
328
329
```javascript
330
// This will be syntax highlighted
331
console.log("Hello World!");
332
```
333
</Markdown>
334
335
// Advanced plugin configuration with options
336
<Markdown
337
remarkPlugins={[
338
remarkGfm,
339
[remarkToc, { heading: "contents", maxDepth: 3 }],
340
[remarkMath, { singleDollarTextMath: false }]
341
]}
342
rehypePlugins={[
343
rehypeSlug,
344
[rehypeHighlight, {
345
detect: true,
346
ignoreMissing: true,
347
subset: ['javascript', 'typescript', 'css']
348
}],
349
[rehypeKatex, {
350
strict: false,
351
trust: false,
352
macros: {
353
"\\RR": "\\mathbb{R}",
354
"\\NN": "\\mathbb{N}"
355
}
356
}]
357
]}
358
remarkRehypeOptions={{
359
allowDangerousHtml: true,
360
clobberPrefix: 'user-content-',
361
footnoteLabel: 'Footnotes',
362
footnoteLabelTagName: 'h2'
363
}}
364
>
365
# Mathematical Document
366
367
## Contents
368
369
Inline math: $E = mc^2$ and display math:
370
371
$$
372
\int_{-\infty}^{\infty} e^{-x^2} dx = \sqrt{\pi}
373
$$
374
375
```typescript
376
// TypeScript code with syntax highlighting
377
interface User {
378
id: number;
379
name: string;
380
}
381
```
382
</Markdown>
383
384
// Custom plugin example
385
function customRemarkPlugin() {
386
return (tree, file) => {
387
// Transform AST nodes
388
visit(tree, 'text', (node) => {
389
node.value = node.value.replace(/TODO:/g, 'π TODO:');
390
});
391
};
392
}
393
394
<Markdown
395
remarkPlugins={[
396
customRemarkPlugin,
397
[remarkGfm, { singleTilde: false }]
398
]}
399
>
400
TODO: This will be prefixed with an emoji
401
402
~~This strikethrough requires double tildes~~
403
</Markdown>
404
```
405
406
## External Type Dependencies
407
408
```typescript { .api }
409
// From 'hast' package
410
interface Element {
411
type: 'element';
412
tagName: string;
413
properties: Properties;
414
children: Array<Element | Text | Comment>;
415
position?: Position;
416
}
417
418
interface Properties {
419
[key: string]: boolean | number | string | null | undefined | Array<string | number>;
420
}
421
422
interface Text {
423
type: 'text';
424
value: string;
425
position?: Position;
426
}
427
428
interface Comment {
429
type: 'comment';
430
value: string;
431
position?: Position;
432
}
433
434
interface Parents {
435
type: string;
436
children: Array<Element | Text | Comment>;
437
}
438
439
interface Position {
440
start: Point;
441
end: Point;
442
}
443
444
interface Point {
445
line: number;
446
column: number;
447
offset?: number;
448
}
449
450
// From 'unified' package
451
type PluggableList = Array<Pluggable>;
452
type Pluggable = Plugin | Preset | [Plugin, ...Parameters] | [Preset, ...Parameters];
453
type Plugin = (this: Processor, ...parameters: any[]) => Transformer | void;
454
type Preset = { plugins: PluggableList; settings?: Settings };
455
type Transformer = (tree: any, file: any, next?: Function) => any;
456
type Parameters = any[];
457
type Settings = Record<string, any>;
458
459
interface Processor {
460
use(...args: any[]): Processor;
461
parse(document: string | Buffer): any;
462
run(tree: any, file?: any, callback?: Function): any;
463
runSync(tree: any, file?: any): any;
464
}
465
466
// From 'remark-rehype' package
467
interface RemarkRehypeOptions {
468
allowDangerousHtml?: boolean;
469
passThrough?: Array<string>;
470
handlers?: Record<string, Function>;
471
unknownHandler?: Function;
472
clobberPrefix?: string;
473
footnoteLabel?: string;
474
footnoteLabelTagName?: string;
475
footnoteLabelProperties?: Properties;
476
footnoteBackLabel?: string;
477
}
478
479
// From 'vfile' package
480
interface VFile {
481
value: string | Buffer;
482
path?: string;
483
basename?: string;
484
stem?: string;
485
extname?: string;
486
dirname?: string;
487
history: Array<string>;
488
messages: Array<VFileMessage>;
489
data: Record<string, any>;
490
}
491
492
interface VFileMessage {
493
reason: string;
494
line?: number;
495
column?: number;
496
position?: Position;
497
ruleId?: string;
498
source?: string;
499
fatal?: boolean;
500
}
501
```
502
503
## Configuration Options
504
505
```typescript { .api }
506
interface Options {
507
/**
508
* Markdown content to render as a string
509
* @default undefined
510
*/
511
children?: string | null | undefined;
512
513
/**
514
* Custom callback function to programmatically filter elements during processing
515
* Called after allowedElements/disallowedElements filtering
516
* @default undefined
517
*/
518
allowElement?: AllowElement | null | undefined;
519
520
/**
521
* Array of HTML tag names to allow in output (whitelist approach)
522
* Cannot be used together with disallowedElements
523
* @default undefined (allows all elements)
524
*/
525
allowedElements?: ReadonlyArray<string> | null | undefined;
526
527
/**
528
* Array of HTML tag names to remove from output (blacklist approach)
529
* Cannot be used together with allowedElements
530
* @default []
531
*/
532
disallowedElements?: ReadonlyArray<string> | null | undefined;
533
534
/**
535
* Object mapping HTML tag names to custom React components
536
* @default undefined
537
*/
538
components?: Components | null | undefined;
539
540
/**
541
* Array of remark plugins to process markdown before converting to HTML
542
* Plugins can be functions or [plugin, options] tuples
543
* @default []
544
*/
545
remarkPlugins?: PluggableList | null | undefined;
546
547
/**
548
* Array of rehype plugins to process HTML after markdown conversion
549
* Plugins can be functions or [plugin, options] tuples
550
* @default []
551
*/
552
rehypePlugins?: PluggableList | null | undefined;
553
554
/**
555
* Configuration options passed to remark-rehype for markdown to HTML conversion
556
* @default { allowDangerousHtml: true }
557
*/
558
remarkRehypeOptions?: Readonly<RemarkRehypeOptions> | null | undefined;
559
560
/**
561
* Whether to completely ignore HTML in markdown input
562
* When true, HTML tags are treated as plain text
563
* @default false
564
*/
565
skipHtml?: boolean | null | undefined;
566
567
/**
568
* Whether to extract children from disallowed elements instead of removing entirely
569
* When false, disallowed elements and their children are removed
570
* When true, only the element wrapper is removed, children are kept
571
* @default false
572
*/
573
unwrapDisallowed?: boolean | null | undefined;
574
575
/**
576
* Function to transform/sanitize URLs in href, src, and other URL attributes
577
* Receives the URL, attribute name, and containing element
578
* @default defaultUrlTransform
579
*/
580
urlTransform?: UrlTransform | null | undefined;
581
}
582
583
interface HooksOptions extends Options {
584
/**
585
* React content to display while markdown is being processed asynchronously
586
* Only used by MarkdownHooks component for client-side async processing
587
* @default undefined
588
*/
589
fallback?: ReactNode | null | undefined;
590
}
591
```
592
593
## Security Features
594
595
React Markdown prioritizes security with multiple built-in protections:
596
597
- **No dangerouslySetInnerHTML**: All content is processed through React's virtual DOM
598
- **URL Sanitization**: Default `defaultUrlTransform` prevents XSS via malicious URLs
599
- **Element Filtering**: Control exactly which HTML elements are allowed
600
- **Plugin Sandboxing**: Plugins operate within the unified processing pipeline
601
602
**Security Example:**
603
604
```typescript
605
import Markdown, { defaultUrlTransform } from "react-markdown";
606
607
// Highly secure configuration
608
<Markdown
609
// Only allow safe elements
610
allowedElements={[
611
'p', 'strong', 'em', 'ul', 'ol', 'li',
612
'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
613
'blockquote', 'code', 'pre'
614
]}
615
616
// Skip any raw HTML
617
skipHtml={true}
618
619
// Use default URL sanitization
620
urlTransform={defaultUrlTransform}
621
>
622
{untrustedMarkdown}
623
</Markdown>
624
```
625
626
## Error Handling
627
628
React Markdown handles various error conditions gracefully:
629
630
- **Invalid markdown**: Renders as much as possible, invalid syntax is treated as text
631
- **Plugin errors**: Async components (MarkdownHooks, MarkdownAsync) will throw errors
632
- **Type validation**: Deprecated props trigger runtime errors with helpful messages
633
- **Configuration conflicts**: Mutually exclusive options (e.g., allowedElements + disallowedElements) cause errors
634
635
**Error Handling Example:**
636
637
```typescript
638
import React from "react";
639
import { MarkdownHooks } from "react-markdown";
640
641
function SafeMarkdown({ content }: { content: string }) {
642
return (
643
<ErrorBoundary fallback={<div>Failed to render markdown</div>}>
644
<MarkdownHooks
645
fallback={<div>Loading...</div>}
646
remarkPlugins={[/* potentially failing async plugins */]}
647
>
648
{content}
649
</MarkdownHooks>
650
</ErrorBoundary>
651
);
652
}
653
```