MDX cheat sheet
Updated
MDX is markdown plus three additions: JSX tags (<Chart />), expressions in braces ({Math.PI}) and ESM import/export. In exchange it drops three markdown features — indented code, autolinks and raw HTML — and makes < and { reserved characters. Syntax below follows the official mdxjs.com docs for MDX 3; snippets are shown as source only because they need a compiler to run.
Markdown vs MDX: what changes
| You write | Markdown (GFM) | MDX |
|---|---|---|
Indented code (4 spaces) | Code block | Paragraph — use ``` fences |
Bare URL / <https://…> | Autolink | Not a link — write [text](url) |
<div>, <img>, <br> | Raw HTML passed through | JSX — must be well-formed and self-closed (<img />, <br />) |
<!-- comment --> | Hidden | Error — use {/* comment */} |
Literal < or { | Plain text | Escape: \< \{ or {'<'} {'{'} |
class="x" on a tag | Passed through | className="x" in React (attribute names follow the framework) |
style="color:red" | Passed through | style={{color: 'red'}} |
Tables, ~~strike~~, task lists, footnotes | GFM (most renderers) | Off until remark-gfm is added |
YAML front matter | Ignored or parsed by the site | Needs remark-frontmatter (and remark-mdx-frontmatter to expose it) |
import / export lines | Plain text | ESM — must be at the top level, not indented |
{Math.PI} | Plain text | Evaluated expression |
Markdown inside <Tag>…</Tag> | n/a | Inline markdown works on the same line; block markdown needs its own lines inside the tag |
JSX in markdown
Any JSX tag can sit between markdown blocks or inside a paragraph. The preview column shows plain markdown equivalents, since the site renderer does not execute components.
A component in prose
import {Chart} from './chart.js'
# Quarterly numbers
Revenue grew **12%**.
<Chart data={[10, 12, 15]} />
See the <Tooltip tip="year over year">YoY</Tooltip> note.Block JSX goes on its own line between blank lines; inline JSX stays inside the sentence.
Markdown inside JSX: same line vs own lines
<div>*this is emphasis*</div>
<div># this is not a heading</div>
<div>
# this IS a heading
</div>Inline markdown works when text and tags share a line; block syntax (headings, lists) only works on its own lines inside the tag.
Self-closing and well-formed tags
<img src="/pic.png" alt="A picture" />
<br />
<Box>
Content
</Box>Unlike HTML in markdown, every tag must close; <img> without the slash is a syntax error.
Attribute names follow the framework
<section className="note" style={{color: 'tomato'}} aria-label="Note">
Text
</section>React wants className and a style object; copying class="…" style="…" from HTML fails.
Imports and exports
Import components and data
import {Chart} from './chart.js'
import data from './data.json'
import {External} from './some/place.js'import/export must start at column 0 and may contain blank lines; they are hoisted out of the content.
Export values other files can import
export const meta = {title: 'Release notes', date: '2026-10-15'}
export function Note({children}) {
return <aside className="note">{children}</aside>
}
<Note>Locally defined components work too.</Note>Named exports are available to whoever imports the .mdx file; the content itself is the default export.
Layout via default export
export default function Layout({children}) {
return <main className="prose">{children}</main>
}
# This heading renders inside <main>A local default export wraps all content and overrides a components.wrapper passed from outside.
Expressions in braces
Inline and block expressions
Two π is {Math.PI * 2}.
{(function () {
const items = ['a', 'b']
return <ul>{items.map((i) => <li key={i}>{i}</li>)}</ul>
})()}Braces hold any JavaScript expression; multi-statement logic goes in an IIFE.
Props passed to the MDX component
# Hello {props.name.toUpperCase()}
The current year is {props.year}Rendered as <Content name="Mars" year={2026} />; the document reads them as props.
Comments and escapes
{/* A comment, invisible in output */}
Templates use \{braces\} and 1 \< 2, or {'{'}braces{'}'} and 1 {'<'} 2.HTML comments are not allowed; \< and \{ (or string expressions) are the only ways to print those characters outside code.
Components as headings and layouts
Replace markdown elements from outside
import Content from './post.mdx'
<Content
components={{
h1: (props) => <h1 className="title" {...props} />,
a: Link,
code: Code,
wrapper: ({children}) => <article>{children}</article>,
}}
/>Keys named after HTML tags (h1, a, pre, code, img) swap the element markdown generates; wrapper wraps everything.
Map h1 to h2 in one line
<Content components={{h1: 'h2'}} />A string value works too — useful when the page template already renders the H1.
Provider for nested MDX (React)
import {MDXProvider} from '@mdx-js/react'
<MDXProvider components={{h1: Title}}>
<Content />
</MDXProvider>Only needed when MDX files import other MDX files; otherwise pass components directly (Next.js uses mdx-components.tsx instead).
Gotchas
No indented code blocks
console.log(1) // this is a paragraph in MDX!
```js
console.log(1) // this is a code block
```Four-space indentation is ignored so JSX can be indented; always use fences.
No autolinks
https://mdxjs.com is plain text.
[mdxjs.com](https://mdxjs.com) is a link.Bare URLs and <https://…> are not links in MDX; <https://…> is even a JSX parse error.
Tags cannot span blocks
<a href="/x">
# Heading inside a link
</a>An opening tag in one paragraph and its closing tag in another raise a misnesting error; keep the whole element in one block.
GFM needs a plugin
import remarkGfm from 'remark-gfm'
// compile(source, {remarkPlugins: [remarkGfm]})
| a | b |
|---|---|
| 1 | 2 |
~~strike~~ - [x] taskWithout remark-gfm the table is a paragraph of pipes and the tildes are literal.
MDX 1 vs 2 vs 3
| Change | MDX 2 (2022) | MDX 3 (2023) |
|---|---|---|
| Syntax | JSX no longer needs blank lines around it and may be indented; expressions {…} added; markdown ambiguities (indented code, autolinks, HTML) removed | Unchanged |
| GFM | Off by default; add remark-gfm | Same |
| Packages | ESM-only (no require()) | Node 16+ required |
| Components | inlineCode removed (use code); dotted names like ol.li removed | MDXContext and withMDXComponents replaced by useMDXComponents |
| Options | — | useDynamicImport removed (now default); baseUrl needed for evaluate/run when the file uses import/export |
| Loaders | — | @mdx-js/register replaced by @mdx-js/node-loader; classic JSX runtime deprecated in favour of automatic |
Sources: migrating to MDX 2 and migrating to MDX 3.
Frameworks: Next.js, Docusaurus, Astro and others
| Framework | Package | Setup | Notes |
|---|---|---|---|
| Next.js (App Router) | @next/mdx | Wrap next.config with createMDX(); add md and mdx to pageExtensions; add mdx-components.tsx for custom components. | mdxjs.com advises against providerImportSource and @mdx-js/react with Next. |
| Docusaurus | built in | Write .mdx in docs/ or blog/; import components at the top of the file. | Docusaurus 3 uses MDX 3; front matter and admonitions (:::note) are supported. |
| Astro | @astrojs/mdx | npx astro add mdx; put .mdx in src/pages or a content collection. | Astro components and framework components can be used; expressions run at build time. |
| Vite | @mdx-js/rollup | Add mdx() to plugins in vite.config.js; with @vitejs/plugin-react use enforce: "pre". | Same plugin serves Rollup builds. |
| Gatsby | gatsby-plugin-mdx | Add the plugin to gatsby-config and source .mdx files. | Gatsby maintains its own integration. |
| Any bundler / Node | @mdx-js/mdx, @mdx-js/node-loader, @mdx-js/esbuild, @mdx-js/loader (webpack) | compile() or evaluate() from @mdx-js/mdx; loaders for esbuild, webpack and Node. | Core packages are ESM-only since MDX 2. |
Source: mdxjs.com getting started.
What is MDX?
MDX is markdown that can contain JSX, JavaScript expressions in curly braces, and ESM import/export statements. A .mdx file compiles to a JavaScript component, so React (or Preact, Vue, Svelte via the JSX runtime) components can be used inline in prose. Markdown support is CommonMark by default; GFM tables, task lists and footnotes need the remark-gfm plugin.
How do I use a React component inside MDX?
Import it at the top of the file and write it as a JSX tag anywhere in the content: import {Chart} from './chart.js' followed by <Chart data={[1, 2, 3]} /> on its own line. Components can also be passed in through the components prop of the compiled MDX component, or defined locally with export.
Why does MDX fail on a < or { character in my text?
In MDX, < starts a JSX tag and { starts an expression, so a bare "a < b" or "use {braces}" is a syntax error. Escape them as \< and \{, or write them as expressions: {'<'} and {'{'}. Inside inline code and fenced code blocks they are fine without escaping.
Why does an HTML comment break MDX?
HTML syntax does not work in MDX; it is replaced by JSX. <!-- comment --> is therefore invalid. Use a JavaScript comment inside braces instead: {/* comment */}. The same rule means <img> must be self-closed as <img />.
What markdown does not work in MDX?
Three things from regular markdown are turned off: indented code blocks (four-space indentation is a paragraph, so JSX can be indented), autolinks (write [text](https://url) instead of a bare URL or <https://url>) and HTML (use JSX). GFM extensions such as tables and strikethrough are off unless remark-gfm is added.
What changed between MDX 2 and MDX 3?
MDX 3 (2023) is a small release: Node 16 or later is required, the useDynamicImport option was removed because its behaviour became the default, evaluate/run need a baseUrl when the MDX uses import/export, MDXContext and withMDXComponents are replaced by useMDXComponents, and @mdx-js/register is replaced by @mdx-js/node-loader. The syntax itself did not change. MDX 2 (2022) was the big break: packages became ESM-only, GFM became opt-in, the inlineCode component and dotted component names like ol.li were removed, and JSX no longer needs blank lines around it.
How do I add a layout to an MDX file?
Export a default function that receives children: export default function Layout({children}) { return <main>{children}</main> }. Alternatively pass the layout as components.wrapper from the outside; a local default export takes precedence over the wrapper.
Related
Markdown cheat sheet · Pandoc markdown · Front matter generator · Markdown lint · Markdown to HTML · Code block guide · Comment guide