Component architecture
The Markdown component uses a two-level rendering system:Creating reusable component sets
Define component sets for consistent styling across your application:import type { MarkdownComponents } from "react-markdown-parser";
// Define your component library
export const blogComponents: Partial<MarkdownComponents> = {
Heading: ({ level, children }) => {
const sizes = {
1: "text-4xl font-bold mt-8 mb-4",
2: "text-3xl font-semibold mt-6 mb-3",
3: "text-2xl font-semibold mt-4 mb-2",
4: "text-xl font-medium mt-3 mb-2",
5: "text-lg font-medium mt-2 mb-1",
6: "text-base font-medium mt-2 mb-1",
};
const Heading = `h${level}` as const;
return <Heading className={sizes[level]}>{children}</Heading>;
},
Paragraph: ({ children }) => (
<p className="my-4 text-gray-800 leading-7">{children}</p>
),
// ... more components
};
// Use across your app
import { Markdown } from "react-markdown-parser";
import { blogComponents } from "./components";
export function BlogPost({ content }: { content: string }) {
return <Markdown content={content} components={blogComponents} />;
}
Interactive code blocks
Add copy buttons, line numbers, and syntax highlighting:Create CodeBlock component
"use client";
import { useState } from "react";
import { CopyToClipboard } from "react-copy-to-clipboard";
import { codeToHtml } from "shiki";
export function InteractiveCodeBlock({
content,
info
}: {
content: string;
info?: string;
}) {
const [copied, setCopied] = useState(false);
const [html, setHtml] = useState<string>("");
// Highlight code on mount
useEffect(() => {
codeToHtml(content, {
lang: info || "text",
theme: "github-dark",
}).then(setHtml);
}, [content, info]);
const handleCopy = () => {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
return (
<div className="relative group">
<div className="flex items-center justify-between bg-gray-800 px-4 py-2 rounded-t-md">
<span className="text-sm text-gray-300">{info || "code"}</span>
<CopyToClipboard text={content} onCopy={handleCopy}>
<button className="text-sm text-gray-300 hover:text-white">
{copied ? "Copied!" : "Copy"}
</button>
</CopyToClipboard>
</div>
<div
className="overflow-x-auto rounded-b-md"
dangerouslySetInnerHTML={{ __html: html }}
/>
</div>
);
}
Enhanced table components
Add sorting, filtering, and responsive behavior:"use client";
import { useState } from "react";
import type { ReactNode } from "react";
type TableProps = {
head: {
cells: { children: ReactNode; align?: "left" | "right" | "center" }[];
};
body: {
rows: {
cells: { children: ReactNode; align?: "left" | "right" | "center" }[];
}[];
};
};
export function SortableTable({ head, body }: TableProps) {
const [sortColumn, setSortColumn] = useState<number | null>(null);
const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc");
const handleSort = (columnIndex: number) => {
if (sortColumn === columnIndex) {
setSortDirection(sortDirection === "asc" ? "desc" : "asc");
} else {
setSortColumn(columnIndex);
setSortDirection("asc");
}
};
const sortedRows = [...body.rows].sort((a, b) => {
if (sortColumn === null) return 0;
const aCell = a.cells[sortColumn];
const bCell = b.cells[sortColumn];
// Simple text comparison (you can enhance this)
const aText = String(aCell?.children);
const bText = String(bCell?.children);
const comparison = aText.localeCompare(bText);
return sortDirection === "asc" ? comparison : -comparison;
});
return (
<div className="overflow-x-auto">
<table className="min-w-full border-collapse">
<thead className="bg-gray-50 border-b-2">
<tr>
{head.cells.map((cell, index) => (
<th
key={index}
align={cell.align}
className="px-4 py-2 text-left cursor-pointer hover:bg-gray-100"
onClick={() => handleSort(index)}
>
<div className="flex items-center gap-2">
{cell.children}
{sortColumn === index && (
<span>{sortDirection === "asc" ? "↑" : "↓"}</span>
)}
</div>
</th>
))}
</tr>
</thead>
<tbody>
{sortedRows.map((row, rowIndex) => (
<tr key={rowIndex} className="border-b hover:bg-gray-50">
{row.cells.map((cell, cellIndex) => (
<td
key={cellIndex}
align={cell.align}
className="px-4 py-2"
>
{cell.children}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
);
}
Accessible link components
Implement proper accessibility features:import type { ReactNode } from "react";
export function AccessibleLink({
href,
title,
children,
}: {
href: string;
title?: string;
children: ReactNode;
}) {
const isExternal = href.startsWith("http") && !href.includes(window.location.hostname);
const isHash = href.startsWith("#");
const isEmail = href.startsWith("mailto:");
const isPhone = href.startsWith("tel:");
// Build aria-label for screen readers
const ariaLabel =
isExternal ? `${children} (opens in new tab)` :
isEmail ? `Send email to ${children}` :
isPhone ? `Call ${children}` :
undefined;
return (
<a
href={href}
title={title}
aria-label={ariaLabel}
target={isExternal ? "_blank" : undefined}
rel={isExternal ? "noopener noreferrer" : undefined}
className="text-blue-600 hover:text-blue-800 underline focus:outline-none focus:ring-2 focus:ring-blue-500 rounded"
>
{children}
{isExternal && (
<svg
className="inline-block w-4 h-4 ml-1"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
/>
</svg>
)}
</a>
);
}
Lazy-loaded images
Optimize image loading with Next.js Image or lazy loading:import Image from "next/image";
export function OptimizedImage({
href,
alt,
title,
}: {
href: string;
alt: string;
title?: string;
}) {
return (
<div className="my-6">
<Image
src={href}
alt={alt}
title={title}
width={800}
height={600}
className="rounded-lg"
loading="lazy"
/>
</div>
);
}
Collapsible blockquotes
Create expandable callouts:"use client";
import { useState } from "react";
import type { ReactNode } from "react";
export function CollapsibleBlockquote({ children }: { children: ReactNode }) {
const [isExpanded, setIsExpanded] = useState(true);
return (
<blockquote className="my-4 border-l-4 border-blue-500 bg-blue-50 rounded-r-lg">
<button
onClick={() => setIsExpanded(!isExpanded)}
className="w-full px-4 py-2 text-left font-medium flex items-center justify-between hover:bg-blue-100"
>
<span>Note</span>
<svg
className={`w-5 h-5 transition-transform ${isExpanded ? "rotate-180" : ""}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 9l-7 7-7-7"
/>
</svg>
</button>
{isExpanded && (
<div className="px-4 pb-4">{children}</div>
)}
</blockquote>
);
}
Heading anchor links
Add auto-generated anchor links to headings:import { ReactNode } from "react";
function slugify(text: string): string {
return text
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/(^-|-$)/g, "");
}
export function HeadingWithAnchor({
level,
children,
}: {
level: 1 | 2 | 3 | 4 | 5 | 6;
children: ReactNode;
}) {
const Heading = `h${level}` as const;
const text = String(children);
const id = slugify(text);
return (
<Heading id={id} className="group relative">
{children}
<a
href={`#${id}`}
className="ml-2 text-gray-400 opacity-0 group-hover:opacity-100 transition-opacity"
aria-label={`Link to ${text}`}
>
#
</a>
</Heading>
);
}
Context-aware components
Access surrounding context in custom components:import { createContext, useContext, type ReactNode } from "react";
import { Markdown } from "react-markdown-parser";
const ArticleContext = createContext<{
author?: string;
publishDate?: string;
}>({});
export function ArticleProvider({
author,
publishDate,
children,
}: {
author: string;
publishDate: string;
children: ReactNode;
}) {
return (
<ArticleContext.Provider value={{ author, publishDate }}>
{children}
</ArticleContext.Provider>
);
}
function ContextAwareLink({
href,
title,
children,
}: {
href: string;
title?: string;
children: ReactNode;
}) {
const { author } = useContext(ArticleContext);
// Add author info to internal links
const enhancedTitle = href.startsWith("/") && author
? `${title || ""} by ${author}`.trim()
: title;
return (
<a href={href} title={enhancedTitle} className="text-blue-600">
{children}
</a>
);
}
// Usage
export function Article({ content, author, publishDate }: {
content: string;
author: string;
publishDate: string;
}) {
return (
<ArticleProvider author={author} publishDate={publishDate}>
<Markdown
content={content}
components={{
Link: ContextAwareLink,
}}
/>
</ArticleProvider>
);
}
Performance optimization
Memoize expensive component operations:import { memo } from "react";
import type { ReactNode } from "react";
// Memoize components that render frequently
export const MemoizedParagraph = memo(function Paragraph({
children
}: {
children: ReactNode
}) {
return <p className="my-4">{children}</p>;
});
export const MemoizedCodeBlock = memo(function CodeBlock({
content,
info,
}: {
content: string;
info?: string;
}) {
// Expensive syntax highlighting
const highlighted = useMemo(
() => highlightCode(content, info),
[content, info]
);
return <pre dangerouslySetInnerHTML={{ __html: highlighted }} />;
});
Memoize components that perform expensive operations like syntax highlighting or complex rendering logic.
Component composition
Combine multiple component sets:import type { MarkdownComponents } from "react-markdown-parser";
const baseComponents: Partial<MarkdownComponents> = {
Heading: ({ level, children }) => { /* ... */ },
Paragraph: ({ children }) => { /* ... */ },
};
const interactiveComponents: Partial<MarkdownComponents> = {
CodeBlock: InteractiveCodeBlock,
Table: SortableTable,
};
const accessibilityComponents: Partial<MarkdownComponents> = {
Link: AccessibleLink,
Image: OptimizedImage,
};
// Merge component sets
export const fullComponents: Partial<MarkdownComponents> = {
...baseComponents,
...interactiveComponents,
...accessibilityComponents,
};
Testing custom components
Test your components with the Markdown component:import { render, screen } from "@testing-library/react";
import { Markdown } from "react-markdown-parser";
import { InteractiveCodeBlock } from "./InteractiveCodeBlock";
test("renders code block with copy button", () => {
render(
<Markdown
content="```js\nconsole.log('test');\n```"
components={{ CodeBlock: InteractiveCodeBlock }}
/>
);
expect(screen.getByText("Copy")).toBeInTheDocument();
expect(screen.getByText(/console\.log/)).toBeInTheDocument();
});
Next steps
API reference
Complete component API documentation
Custom renderers
Browse all available component overrides