MDX Notes
How to build your own React components for MDX — from MDXProvider setup to overriding headings, links, and code blocks
The whole point of MDX is mixing React components into your markdown. You're not stuck with plain <h1> and <p> tags — you can replace every single HTML element with your own component, and add entirely new ones like <Steps> or <Callout>.
Here's how components work in MDX. There are three ways to get them in:
- Import directly — Write
import { Alert } from './Alert'at the top of an MDX file - Provide via MDXProvider — Make them available everywhere, no imports needed
- Map to HTML elements — Replace what
#,>, ```, links, etc. render as
Let's dig into each one.
MDXProvider: Global Component Registration
If you don't want authors importing stuff in every single file, use the MDXProvider. It makes components available globally:
// app/mdx-provider.jsx (Next.js App Router)
import { MDXProvider } from '@mdx-js/react';
import { Alert } from '@/components/Alert';
import { Card, CardGrid } from '@/components/Card';
import { Tabs, TabList, Tab, TabPanel } from '@/components/Tabs';
import { Accordion, AccordionGroup } from '@/components/Accordion';
import { CodeBlock } from '@/components/CodeBlock';
import { Callout } from '@/components/Callout';
import { Steps } from '@/components/Steps';
const components = {
// Custom components (used as <Alert>, <Card>, etc. in MDX)
Alert,
Card,
CardGrid,
Tabs,
TabList,
Tab,
TabPanel,
Accordion,
AccordionGroup,
CodeBlock,
Callout,
Steps,
};
export function MDXProviderWrapper({ children }) {
return (
<MDXProvider components={components}>
{children}
</MDXProvider>
);
}Next.js App Router (mdx-components.js)
Next.js 13+ has its own pattern — a special mdx-components.js file at the project root:
Component Mapping: Overriding Default Elements
This is where things get interesting. When you write # Hello in MDX, it renders <h1>Hello</h1> by default. Component mapping lets you swap that <h1> with anything you want — a heading with auto-generated anchor links, custom fonts, whatever.
Overriding Headings
Here's a heading component that auto-generates slugs and shows a hover link:
Overriding Paragraphs
// components/CustomParagraph.jsx
export function CustomParagraph({ children, ...props }) {
return (
<p
className="text-gray-700 dark:text-gray-300 leading-relaxed mb-4"
{...props}
>
{children}
</p>
);
}Overriding Links
This one's practical — external links get target="_blank" and a little arrow icon automatically:
// components/CustomLink.jsx
import React from 'react';
export function CustomLink({ href, children, ...props }) {
const isExternal = href?.startsWith('http');
if (isExternal) {
return (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 dark:text-blue-400 underline hover:text-blue-800 dark:hover:text-blue-300 inline-flex items-center gap-1"
{...props}
>
{children}
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" 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>
);
}
return (
<a
href={href}
className="text-blue-600 dark:text-blue-400 underline hover:text-blue-800 dark:hover:text-blue-300"
{...props}
>
{children}
</a>
);
}Overriding Code Blocks
This gives you syntax highlighting with prism-react-renderer plus a copy button:
Overriding Blockquotes
// components/CustomBlockquote.jsx
export function CustomBlockquote({ children, ...props }) {
return (
<blockquote
className="border-l-4 border-blue-400 bg-blue-50 dark:bg-blue-900/20 pl-4 py-3 pr-4 my-4 rounded-r-lg italic text-gray-700 dark:text-gray-300"
{...props}
>
{children}
</blockquote>
);
}Overriding Tables
// components/CustomTable.jsx
export function CustomTable({ children, ...props }) {
return (
<div className="overflow-x-auto my-4 rounded-lg border border-gray-200 dark:border-gray-700">
<table className="w-full text-sm" {...props}>
{children}
</table>
</div>
);
}
export function CustomTh({ children, ...props }) {
return (
<th
className="px-4 py-3 bg-gray-50 dark:bg-gray-800 text-left font-semibold text-gray-700 dark:text-gray-300 border-b border-gray-200 dark:border-gray-700"
{...props}
>
{children}
</th>
);
}
export function CustomTd({ children, ...props }) {
return (
<td
className="px-4 py-3 border-b border-gray-100 dark:border-gray-800 text-gray-600 dark:text-gray-400"
{...props}
>
{children}
</td>
);
}Complete Component Mapping
Here's the full thing — every HTML element you can override, all in one object:
Wrapper Components
A wrapper wraps your *entire* MDX content. Good for adding a table of contents, reading time, navigation — stuff that goes around every page:
// components/MDXWrapper.jsx
import React from 'react';
export function MDXWrapper({ children, meta }) {
return (
<article className="prose prose-lg dark:prose-invert max-w-none">
{/* Table of contents */}
<TableOfContents />
{/* Reading time estimate */}
{meta?.readingTime && (
<p className="text-sm text-gray-500 mb-8">
📖 {meta.readingTime} min read
</p>
)}
{/* Main content */}
<div className="mdx-content">
{children}
</div>
{/* Footer navigation */}
<DocNavigation prev={meta?.prev} next={meta?.next} />
</article>
);
}Register it with the special wrapper key:
const components = {
wrapper: MDXWrapper,
// ... other components
};Creating Your Own Components: Step-by-Step
Here's the workflow from idea to working component.
Step 1: Design the Component API
Think about how an author will actually write this in MDX. Keep it dead simple:
{/* Good — clear, minimal API */}
<Steps>
<Step title="Install dependencies">
Run `npm install` to get started.
</Step>
<Step title="Configure settings">
Create a config file with your preferences.
</Step>
</Steps>Step 2: Build the React Component
Step 3: Register the Component
Step 4: Document the Component
Create a reference page showing usage, props, and examples (like this page!).
Design System Integration
If your team already uses Chakra UI, Material UI, Radix, etc., you can map MDX elements straight to your design system. No custom components needed.
Chakra UI Integration
Radix UI + Tailwind Integration
Props Documentation Table for Component Mapping
| Element Key | Markdown Syntax | Rendered As | Common Overrides | ||
|---|---|---|---|---|---|
h1–h6 | # Heading | <h1>–<h6> | Anchor links, custom styling | ||
p | Paragraph text | <p> | Typography, spacing | ||
a | text | <a> | External link icons, routing | ||
code | ` inline ` | <code> | Syntax highlighting, copy button | ||
pre | ``` `code` ``` | <pre> | Full code blocks with toolbars | ||
blockquote | > quote | <blockquote> | Styled callouts | ||
ul / ol | - item / 1. item | <ul> / <ol> | Custom bullets, spacing | ||
li | List item | <li> | Check marks, icons | ||
table | `\ | col \ | ` | <table> | Responsive wrapper, styling |
th / td | Table cells | <th> / <td> | Alignment, padding | ||
img | !alt | <img> | Lazy loading, captions, zoom | ||
hr | --- | <hr> | Custom dividers | ||
wrapper | Entire document | <div> | Layout, table of contents |
Accessibility Notes
A few things to keep in mind when you're overriding elements:
- Keep semantic HTML — If you override
h2, your component should still render an actual<h2>tag (or use the right ARIA role). Don't break the document outline.
- External links — Tell users (and screen readers) when a link opens a new window. The icon helps visually, but add
aria-labeltext too.
- Code copy buttons — They usually just show an icon. Add an
aria-labelso screen readers know what the button does.
- Images — Always pass through the
alttext. For complex images with captions, addaria-describedby.
- Tables in scroll containers — When you wrap a table in an overflow-scroll div, add
role="region"andaria-labelto the wrapper so screen readers announce it.
- Heading hierarchy — Don't let authors skip from
h2toh5. In dev mode, you could log a warning when heading levels jump.
Advanced: Dynamic Component Loading
If you have a lot of components and don't want to bloat the bundle, load heavy ones dynamically:
Summary
Custom components are what make MDX more than just "Markdown that accepts JSX." You register components globally with MDXProvider, override default HTML elements with component mapping, and wrap pages with layout components. The trick is designing APIs that feel natural inside markdown — simple props, clear names, composable patterns. When it works well, content authors just write and the components handle styling, interactivity, and accessibility for them.
Exam Focus
Revise definitions, diagrams, examples, and short-answer points for Creating Custom Components for MDX.
Interview Use
Prepare one clear explanation, one practical example, and one common mistake for this MDX Tutorial topic.
Search Terms
mdx-tutorial, mdx tutorial, mdx, tutorial, components, custom, creating custom components for mdx
Related MDX Tutorial Topics