The Markdown component accepts a components prop that allows you to customize how different markdown elements are rendered. You can override any combination of block and inline node renderers.
Overview
import { Markdown } from "react-markdown-parser";
export function CustomArticle({ content }: { content: string }) {
return (
<Markdown
content={content}
components={{
CodeBlock: ({ content, info }) => {
// Custom code block renderer
return (
<pre data-lang={info}>
<code>{content}</code>
</pre>
);
},
Link: ({ href, children }) => {
// Custom link renderer
return (
<a href={href} rel="noreferrer">
{children}
</a>
);
},
}}
/>
);
}
Block node components
Block components render block-level markdown elements. Each receives specific props based on the markdown node type.
Table
Renders markdown tables with header and body cells.
Table header configurationArray of header cellsRendered content of the header cell
head.cells[].align
'left' | 'right' | 'center' | undefined
Text alignment for the column
Table body configurationArray of table rowsArray of cells in this rowbody.rows[].cells[].children
Rendered content of the cell
body.rows[].cells[].align
'left' | 'right' | 'center' | undefined
Text alignment for the cell
Table: ({ head, body }) => (
<table className="custom-table">
<thead>
<tr>
{head.cells.map((cell, i) => (
<th key={i} style={{ textAlign: cell.align }}>
{cell.children}
</th>
))}
</tr>
</thead>
<tbody>
{body.rows.map((row, i) => (
<tr key={i}>
{row.cells.map((cell, j) => (
<td key={j} style={{ textAlign: cell.align }}>
{cell.children}
</td>
))}
</tr>
))}
</tbody>
</table>
)
CodeBlock
Renders fenced code blocks.
The info string from the code fence (typically the language identifier)
CodeBlock: ({ content, info }) => {
const language = info || 'text';
return (
<pre className={`language-${language}`}>
<code>{content}</code>
</pre>
);
}
Blockquote
Renders blockquote elements.
The rendered child nodes (paragraphs, lists, etc.) inside the blockquote
Blockquote: ({ children }) => (
<blockquote className="border-l-4 pl-4 italic">
{children}
</blockquote>
)
List
Renders ordered and unordered lists. The props differ based on list type.
Ordered list props:
Indicates this is an ordered list
Array of list itemsRendered content of the list item
The starting number for ordered lists (default: 1)
Unordered list props:
Indicates this is an unordered list
Array of list itemsRendered content of the list item
List: (props) => {
if (props.type === 'ordered') {
return (
<ol start={props.start} className="list-decimal">
{props.items.map((item, i) => (
<li key={i}>{item.children}</li>
))}
</ol>
);
}
return (
<ul className="list-disc">
{props.items.map((item, i) => (
<li key={i}>{item.children}</li>
))}
</ul>
);
}
Heading
Renders headings from h1 to h6.
level
1 | 2 | 3 | 4 | 5 | 6
required
The heading level
The rendered heading text and inline elements
Heading: ({ level, children }) => {
const id = slugify(children);
const HeadingTag = `h${level}` as const;
return (
<HeadingTag id={id} className={`heading-${level}`}>
{children}
</HeadingTag>
);
}
Paragraph
Renders paragraph elements.
The rendered inline content of the paragraph
Paragraph: ({ children }) => (
<p className="my-4 leading-relaxed">
{children}
</p>
)
ThematicBreak
Renders horizontal rules (thematic breaks).
This component receives no props.
ThematicBreak: () => (
<hr className="my-8 border-gray-300" />
)
HtmlBlock
Renders raw HTML blocks.
Be cautious when rendering HTML blocks from untrusted sources. Consider sanitizing the content.
HtmlBlock: ({ content }) => (
<div dangerouslySetInnerHTML={{ __html: sanitize(content) }} />
)
Inline node components
Inline components render inline-level markdown elements within block content.
Text
Renders plain text nodes.
Text: ({ text }) => <>{text}</>
CodeSpan
Renders inline code.
CodeSpan: ({ text }) => (
<code className="bg-gray-100 px-1 py-0.5 rounded">
{text}
</code>
)
Emphasis
Renders emphasized (italic) text.
The rendered child inline nodes
Emphasis: ({ children }) => (
<em className="italic">{children}</em>
)
Strong
Renders strong (bold) text.
The rendered child inline nodes
Strong: ({ children }) => (
<strong className="font-bold">{children}</strong>
)
Link
Renders hyperlinks.
The optional link title attribute
The default Link renderer includes URL validation to prevent XSS attacks. If you override this component, ensure you implement similar security measures.
Link: ({ href, title, children }) => {
// Internal links use Next.js routing
if (href.startsWith('/')) {
return <NextLink href={href} title={title}>{children}</NextLink>;
}
// External links
return (
<a href={href} title={title} target="_blank" rel="noopener noreferrer">
{children}
</a>
);
}
Image
Renders images.
The image alt text (extracted from the markdown image text)
The optional image title attribute
The default Image renderer includes URL validation. Ensure custom implementations maintain security.
Image: ({ href, alt, title }) => (
<img
src={href}
alt={alt}
title={title}
loading="lazy"
className="max-w-full h-auto"
/>
)
HardBreak
Renders explicit line breaks.
This component receives no props.
SoftBreak
Renders soft line breaks (typically converted to spaces).
This component receives no props. The default renderer returns null.
SoftBreak: () => <>{' '}</>
Html
Renders inline HTML.
Be cautious when rendering inline HTML from untrusted sources. Consider sanitizing the content.
Html: ({ content }) => (
<span dangerouslySetInnerHTML={{ __html: sanitize(content) }} />
)
Complete example
Syntax highlighting
Custom styling
Link validation
import { Markdown } from "react-markdown-parser";
import { highlight } from "highlight.js";
export function ArticleWithHighlighting({ content }: { content: string }) {
return (
<Markdown
content={content}
components={{
CodeBlock: ({ content, info }) => {
const language = info || "plaintext";
const highlighted = highlight.highlight(content, { language });
return (
<pre className={`hljs language-${language}`}>
<code dangerouslySetInnerHTML={{ __html: highlighted.value }} />
</pre>
);
},
}}
/>
);
}
import { Markdown } from "react-markdown-parser";
export function StyledArticle({ content }: { content: string }) {
return (
<Markdown
content={content}
components={{
Heading: ({ level, children }) => {
const className = {
1: "text-4xl font-bold mt-8 mb-4",
2: "text-3xl font-bold mt-6 mb-3",
3: "text-2xl font-semibold mt-4 mb-2",
4: "text-xl font-semibold mt-3 mb-2",
5: "text-lg font-semibold mt-2 mb-1",
6: "text-base font-semibold mt-2 mb-1",
}[level];
const Heading = `h${level}` as const;
return <Heading className={className}>{children}</Heading>;
},
Paragraph: ({ children }) => (
<p className="my-4 text-gray-700 leading-relaxed">{children}</p>
),
Link: ({ href, children }) => (
<a href={href} className="text-blue-600 hover:underline">
{children}
</a>
),
CodeSpan: ({ text }) => (
<code className="bg-gray-100 px-1.5 py-0.5 rounded text-sm font-mono">
{text}
</code>
),
}}
/>
);
}
import { Markdown } from "react-markdown-parser";
import Link from "next/link";
function isInternalLink(href: string): boolean {
return href.startsWith("/") || href.startsWith("#");
}
function isSafeExternalLink(href: string): boolean {
try {
const url = new URL(href);
return ["http:", "https:"].includes(url.protocol);
} catch {
return false;
}
}
export function SecureArticle({ content }: { content: string }) {
return (
<Markdown
content={content}
components={{
Link: ({ href, title, children }) => {
// Internal links use Next.js router
if (isInternalLink(href)) {
return (
<Link href={href} title={title}>
{children}
</Link>
);
}
// Only render safe external links
if (isSafeExternalLink(href)) {
return (
<a
href={href}
title={title}
target="_blank"
rel="noopener noreferrer"
>
{children}
</a>
);
}
// Unsafe links are rendered as plain text
return <span title="Unsafe link removed">{children}</span>;
},
}}
/>
);
}
Type reference
type MarkdownComponents = BlockNodeComponents & InlineNodeComponents;
interface BlockNodeComponents {
Table: ComponentType<{
head: {
cells: {
children: ReactNode;
align: "left" | "right" | "center" | undefined;
}[];
};
body: {
rows: {
cells: {
children: ReactNode;
align: "left" | "right" | "center" | undefined;
}[];
}[];
};
}>;
CodeBlock: ComponentType<{ content: string; info?: string }>;
Blockquote: ComponentType<{ children: ReactNode }>;
List: ComponentType<
| { type: "ordered"; items: { children: ReactNode }[]; start?: number }
| { type: "unordered"; items: { children: ReactNode }[] }
>;
Heading: ComponentType<{ level: 1 | 2 | 3 | 4 | 5 | 6; children: ReactNode }>;
Paragraph: ComponentType<{ children: ReactNode }>;
ThematicBreak: ComponentType;
HtmlBlock: ComponentType<{ content: string }>;
}
interface InlineNodeComponents {
Text: ComponentType<{ text: string }>;
CodeSpan: ComponentType<{ text: string }>;
Emphasis: ComponentType<{ children: ReactNode }>;
Strong: ComponentType<{ children: ReactNode }>;
Link: ComponentType<{ href: string; title?: string; children: ReactNode }>;
Image: ComponentType<{ href: string; title?: string; alt: string }>;
HardBreak: ComponentType;
SoftBreak: ComponentType;
Html: ComponentType<{ content: string }>;
}