import {Note} from '../_component/note.jsx'
export const info = { author: [ {github: 'wooorm', name: 'Titus Wormer'} ], modified: new Date('2025-01-27'), published: new Date('2021-09-30') } export const navSortSelf = 3
This article explains how to use MDX files in your project. It shows how you can pass props and how to import, define, or pass components. {/* more */} See § Getting started for how to integrate MDX into your project. To understand how the MDX format works, we recommend that you start with § What is MDX.
An integration compiles MDX syntax to JavaScript.
Say we have an MDX document, example.mdx
:
export function Thing() {
return <>World</>
}
# Hello <Thing />
That’s roughly turned into the following JavaScript. The below might help to form a mental model:
/* @jsxRuntime automatic */
/* @jsxImportSource react */
export function Thing() {
return <>World</>
}
export default function MDXContent() {
return <h1>Hello <Thing /></h1>
}
Some observations:
- The output is serialized JavaScript that still needs to be evaluated
- A comment is injected to configure how JSX is handled
- It’s a complete file with import/exports
- A component (
MDXContent
) is exported
The actual output is:
// @noErrors
import {Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs} from 'react/jsx-runtime'
export function Thing() {
return _jsx(_Fragment, {children: 'World'})
}
function _createMdxContent(props) {
const _components = {h1: 'h1', ...props.components}
return _jsxs(_components.h1, {children: ['Hello ', _jsx(Thing, {})]})
}
export default function MDXContent(props = {}) {
const {wrapper: MDXLayout} = props.components || {}
return MDXLayout
? _jsx(MDXLayout, {...props, children: _jsx(_createMdxContent, {...props})})
: _createMdxContent(props)
}
Some more observations:
- JSX is compiled away to function calls and an import of React†
- The content component can be given
{components: {wrapper: MyLayout}}
to wrap all content - The content component can be given
{components: {h1: MyComponent}}
to use something else for the heading
† MDX is not coupled to React. You can also use it with Preact, Vue, Emotion, Theme UI, etc. Both the classic and automatic JSX runtimes are supported.
We just saw that MDX files are compiled to components. You can use those components like any other component in your framework of choice. Take this file:
# Hi!
It could be imported and used in a React app like so:
// @filename: types.d.ts
import type {} from 'mdx'
// @filename: example.jsx
/// <reference lib="dom" />
// ---cut---
import {createRoot} from 'react-dom/client'
import Example from './example.mdx' // Assumes an integration is used to compile MDX -> JS.
const container = document.getElementById('root')
if (!container) throw new Error('Expected `root`')
const root = createRoot(container)
root.render(<Example />)
The main content is exported as the default export. All other values are also exported. Take this example:
export function Thing() {
return <>World</>
}
# Hello <Thing />
It could be imported in the following ways:
// @filename: types.d.ts
declare module '*.mdx' {
export {MDXContent as default} from 'mdx/types';
export function Thing(): unknown;
}
// @filename: example.js
/// <reference types="node" />
// ---cut---
// A namespace import to get everything:
import * as everything from './example.mdx' // Assumes an integration is used to compile MDX -> JS.
console.log(everything) // {Thing: [Function: Thing], default: [Function: MDXContent]}
// Default export shortcut and a named import specifier:
import Content, {Thing} from './example.mdx'
console.log(Content) // [Function: MDXContent]
console.log(Thing) // [Function: Thing]
// Import specifier with another local name:
import {Thing as AnotherName} from './example.mdx'
console.log(AnotherName) // [Function: Thing]
In § What is MDX, we showed that JavaScript expressions, inside curly braces, can be used in MDX:
import {year} from './data.js'
export const name = 'world'
# Hello {name.toUpperCase()}
The current year is {year}
Instead of importing or defining data within MDX, data can also be passed
to MDXContent
.
The passed data is called props
.
Take for example:
# Hello {props.name.toUpperCase()}
The current year is {props.year}
This file could be used as:
// @filename: types.d.ts
import type {} from 'mdx'
// @filename: example.jsx
/// <reference lib="dom" />
// ---cut---
import React from 'react'
import Example from './example.mdx' // Assumes an integration is used to compile MDX -> JS.
// Use a `createElement` call:
console.log(React.createElement(Example, {name: 'Venus', year: 2021}))
// Use JSX:
console.log(<Example name="Mars" year={2022} />)
There is one special prop: components
.
It takes an object mapping component names to components.
Take this example:
# Hello *<Planet />*
It can be imported from JavaScript and passed components like so:
// @filename: types.d.ts
import type {} from 'mdx'
// @filename: example.jsx
/// <reference lib="dom" />
/* @jsxImportSource react */
// ---cut---
import Example from './example.mdx' // Assumes an integration is used to compile MDX -> JS.
console.log(
<Example
components={{
Planet() {
return <span style={{color: 'tomato'}}>Pluto</span>
}
}}
/>
)
You don’t have to pass components. You can also define or import them within MDX:
import {Box, Heading} from 'rebass'
MDX using imported components!
<Box>
<Heading>Here’s a heading</Heading>
</Box>
Because MDX files are components, they can also import each other:
import License from './license.md' // Assumes an integration is used to compile markdown -> JS.
import Contributing from './docs/contributing.mdx'
# Hello world
<License />
---
<Contributing />
Here are some other examples of passing components:
// @filename: types.d.ts
import type {} from 'mdx'
// @filename: example.jsx
/// <reference lib="dom" />
/* @jsxImportSource react */
import Example from './example.mdx'
// @errors: 2322 -- something with React 19 and nested components.
// ---cut---
console.log(
<Example
components={{
// Map `h1` (`# heading`) to use `h2`s.
h1: 'h2',
// Rewrite `em`s (`*like so*`) to `i` with a goldenrod foreground color.
em(props) {
return <i style={{color: 'goldenrod'}} {...props} />
},
// Pass a layout (using the special `'wrapper'` key).
wrapper({components, ...rest}) {
return <main {...rest} />
},
// Pass a component.
Planet() {
return 'Neptune'
},
// This nested component can be used as `<theme.text>hi</theme.text>`
theme: {
text(props) {
return <span style={{color: 'grey'}} {...props} />
}
}
}}
/>
)
The following keys can be passed in components
:
- HTML equivalents for the things you write with markdown such as
h1
for# heading
(see § Table of components for examples) wrapper
, which defines the layout (but a local layout takes precedence)- anything else that is a valid JSX identifier (
foo
,Quote
,custom-element
,_
,$x
,a1
) for the things you write with JSX (like<So />
or<like.so />
, note that locally defined components take precedence)‡
‡ The rules for whether a name in JSX (so x
in <x>
) is a literal tag
name (like h1
) or not (like Component
) are as follows:
- if there’s a dot,
it’s a member expression (
<a.b>
→h(a.b)
), which means a reference to the keyb
taken from objecta
- otherwise,
if the name is not a valid JS identifier,
it’s a literal (
<a-b>
→h('a-b')
) - otherwise,
if it starts with a lowercase,
it’s a literal (
<a>
→h('a')
) - otherwise,
it’s a reference (
<A>
→h(A)
)
These keys in components
and the difference between literal tag names and
references is illustrated as follows.
With the following MDX:
* [markdown syntax](#alpha)
* <a href="#bravo">JSX with a lowercase name</a>
* <Link to="#charlie">JSX with a capitalized name</Link>
…passed some components:
// @filename: types.d.ts
import type {} from 'mdx'
// @filename: example.jsx
/// <reference lib="dom" />
/* @jsxImportSource react */
// ---cut---
import Example from './example.mdx'
console.log(
<Example
components={{
a(props) {
return <a {...props} style={{borderTop: '1px dotted', color: 'violet'}} />
},
Link(props) {
return <a href={props.to} children={props.children} style={{borderTop: '1px dashed', color: 'tomato'}} />
}
}}
/>
)
…we’d get:
{
}Observe that the first link (#alpha
) is dotted and violet.
That’s because a
is the HTML equivalent for the markdown syntax being used.
The second link (#bravo
) remains unchanged,
because in JSX syntax a
is a literal tag name.
The third link (#charlie
) is dashed and tomato,
because in JSX syntax Link
is a reference.
There is one special component: the layout. If it is defined, it’s used to wrap all content. A layout can be defined from within MDX using a default export:
export default function Layout({children}) {
return <main>{children}</main>;
}
All the things.
The layout can also be imported and then exported with an export … from
:
export {Layout as default} from './components.js'
The layout can also be passed as components.wrapper
(but a local one takes
precedence).
You probably don’t need a provider. Passing components is typically fine. Providers often only add extra weight. Take for example this file:
# Hello world
Used like so:
// @filename: components.d.ts
import React from 'react'
declare const Heading: {H1: React.ComponentType}
declare const Table: React.ComponentType
// @filename: types.d.ts
import type {} from 'mdx'
// @filename: example.jsx
/// <reference lib="dom" />
// ---cut---
import {createRoot} from 'react-dom/client'
import {Heading, /* … */ Table} from './components.js'
import Post from './post.mdx' // Assumes an integration is used to compile MDX -> JS.
const components = {
h1: Heading.H1,
// …
table: Table
}
const container = document.getElementById('root')
if (!container) throw new Error('Expected `root`')
const root = createRoot(container)
root.render(<Post components={components} />)
That works, those components are used.
But when you’re nesting MDX files (importing them into each other) it can become cumbersome. Like so:
import License from './license.md' // Assumes an integration is used to compile markdown -> JS.
import Contributing from './docs/contributing.mdx'
# Hello world
<License components={props.components} />
---
<Contributing components={props.components} />
To solve this, a context can be used in React, Preact, and Vue. Context provides a way to pass data through the component tree without having to pass props down manually at every level. Set it up like so:
- Install either
@mdx-js/react
,@mdx-js/preact
, or@mdx-js/vue
, depending on what framework you’re using - Configure your MDX integration with
providerImportSource
inProcessorOptions
set to that package, so either'@mdx-js/react'
,'@mdx-js/preact'
, or'@mdx-js/vue'
- Import
MDXProvider
from that package. Use it to wrap your top-most MDX content component and pass it yourcomponents
instead:
+import {MDXProvider} from '@mdx-js/react'
import {createRoot} from 'react-dom/client'
import {Heading, /* … */ Table} from './components/index.js'
import Post from './post.mdx' // Assumes an integration is used to compile MDX -> JS.
@@ -13,4 +14,8 @@ const components = {
const container = document.getElementById('root')
if (!container) throw new Error('Expected `root`')
const root = createRoot(container)
-root.render(<Post components={components} />)
+root.render(
+ <MDXProvider components={components}>
+ <Post />
+ </MDXProvider>
+)
Now you can remove the explicit and verbose component passing:
import License from './license.md' // Assumes an integration is used to compile markdown -> JS.
import Contributing from './docs/contributing.mdx'
# Hello world
-<License components={props.components} />
+<License />
---
-<Contributing components={props.components} />
+<Contributing />
When MDXProvider
s are nested, their components are merged.
Take this example:
// @filename: types.d.ts
import React from 'react'
import type {MDXContent} from 'mdx/types.js'
declare const Component1: React.ComponentType
declare const Component2: React.ComponentType
declare const Component3: React.ComponentType
declare const Component4: React.ComponentType
declare const Content: MDXContent
// @filename: example.jsx
/// <reference lib="dom" />
import {MDXProvider} from '@mdx-js/react'
import React from 'react'
import {Component1, Component2, Component3, Component4, Content} from './types.js'
// ---cut---
console.log(
<MDXProvider components={{h1: Component1, h2: Component2}}>
<MDXProvider components={{h2: Component3, h3: Component4}}>
<Content />
</MDXProvider>
</MDXProvider>
)
…which results in h1
s using Component1
, h2
s using Component3
, and h3
s
using Component4
.
To merge differently or not at all, pass a function to components
.
It’s given the current context components
and what it returns will be used
instead.
In this example the current context components are discarded:
// @filename: types.d.ts
import React from 'react'
import type {MDXContent} from 'mdx/types.js'
declare const Component1: React.ComponentType
declare const Component2: React.ComponentType
declare const Component3: React.ComponentType
declare const Component4: React.ComponentType
declare const Content: MDXContent
// @filename: example.jsx
/// <reference lib="dom" />
import {MDXProvider} from '@mdx-js/react'
import React from 'react'
import {Component1, Component2, Component3, Component4, Content} from './types.js'
// ---cut---
console.log(
<MDXProvider components={{h1: Component1, h2: Component2}}>
<MDXProvider
components={
function () {
return {h2: Component3, h3: Component4}
}
}
>
<Content />
</MDXProvider>
</MDXProvider>
)
…which results in h2
s using Component3
and h3
s using Component4
.
No component is used for h1
.
If you’re not nesting MDX files, or not nesting them often, don’t use providers: pass components explicitly.