Skip to navigation
7-11 minutes read
By Titus Wormer

Getting started

This article explains how to integrate MDX into your project. It shows how to use MDX with your bundler and JSX runtime of choice. To understand how the MDX format works, we recommend that you start with § What is MDX. See § Using MDX when you’re all set up and ready to use MDX.

Contents

Prerequisites

MDX relies on JSX, so it’s required that your project supports JSX as well. Any JSX runtime (React, Preact, Vue, etc.) will do. Note that we do compile JSX to JavaScript for you so you don’t have to set that up.

All @mdx-js/* packages are written in modern JavaScript. A Node.js version of 12.20, 14.14, 16.0, or later is required to use them. Our packages are also ESM only, so they have to be imported instead of required.

Quick start

Bundler

MDX is a language that’s compiled to JavaScript. (We also compile regular markdown to JavaScript.) The easiest way to get started is to use an integration for your bundler if you have one:

  • If you’re using esbuild, install and configure @mdx-js/esbuild
  • If you’re using Rollup (or Vite or WMR, which use it), install and configure @mdx-js/rollup
  • If you’re using webpack (or Create React App (CRA), Next.js, or Vue CLI, which use it), install and configure @mdx-js/loader

You can also use MDX if you’re not using a bundler:

  • If you want to import MDX files in Node.js, you can install and configure @mdx-js/node-loader
  • Otherwise, you can install and use the core compiler @mdx-js/mdx to manually compile MDX files
  • Finally, it’s also possible to evaluate (compile and run) MDX anywhere, with evaluate from @mdx-js/mdx.

For more info on the aforementioned tools, please see their dedicated sections: ¶ Create React App (CRA), ¶ esbuild, ¶ Next.js, ¶ Node.js, ¶ Rollup, ¶ Vite, ¶ Vue CLI, ¶ WMR, ¶ webpack.

There are also community driven integrations. As we’ve just hit a major milestone with v2, they might be out of date with our v2 docs though. See: ¶ Docusaurus, ¶ Gatsby, ¶ Parcel, ¶ Razzle, ¶ React Static, and ¶ Snowpack.

JSX

Now you’ve set up an integration or @mdx-js/mdx itself, it’s time to configure your JSX runtime.

Other JSX runtimes are supported by setting options.jsxImportSource. See also the different options there on how to use the classic JSX runtime and how to define a pragma and pragmaFrag for it.

For more info on the aforementioned tools, please see their dedicated sections: ¶ Emotion, ¶ Preact, ¶ React, ¶ Svelte, ¶ Theme UI, or ¶ Vue.

Editor

Once everything is set up in your project, you can enhance the experience by adding support for MDX in your editor:

Note: we’re looking for help with atom, emacs, and others!

Types

Expand example of typed imports

First install the package:

Shell
npm install @types/mdx

…TypeScript should automatically pick it up:

example.js
import Post from './post.mdx' // `Post` is now typed.

All our APIs are fully typed with TypeScript.

To enable types for imported .mdx, .md, etcetera files, you should make sure the TypeScript JSX namespace is typed. This is done by installing and using the types of your framework, such as @types/react. Then you can install and use @types/mdx, which adds types to import statements of supported files.

Security

Please note that MDX is a programming language. If you trust your authors, that’s fine. But be extremely careful with user content and don’t let random people from the internet write MDX. If you do, you might want to look into using <iframe>s with sandbox, but security is hard, and that doesn’t seem to be 100%. For Node, vm2 sounds interesting. But you should probably also sandbox the whole OS using Docker or similar, perform rate limiting, and make sure processes can be killed when taking too long.

Integrations

Bundlers

esbuild
Expand example
example.js
import esbuild from 'esbuild'
import mdx from '@mdx-js/esbuild'

await esbuild.build({
  entryPoints: ['index.mdx'],
  outfile: 'output.js',
  format: 'esm',
  plugins: [mdx({/* jsxImportSource: …, otherOptions… */})]
})

We support esbuild. Install and configure the esbuild plugin @mdx-js/esbuild. This plugin has an additional option allowDangerousRemoteMdx. Configure your JSX runtime depending on which one you use (React, Preact, Vue, etc.).

If you use more modern JavaScript features than what your users support, configure esbuild’s target.

Rollup
Expand example
rollup.config.js
import mdx from '@mdx-js/rollup'
import {babel} from '@rollup/plugin-babel'

export default {
  // …
  plugins: [
    // …
    mdx({/* jsxImportSource: …, otherOptions… */})
    // Babel is optional.
    babel({
      // Also run on what used to be `.mdx` (but is now JS):
      extensions: ['.js', '.jsx', '.cjs', '.mjs', '.md', '.mdx'],
      // Other options…
    })
  ]
}

We support Rollup. Install and configure the Rollup plugin @mdx-js/rollup. This plugin has additional options include, exclude. Configure your JSX runtime depending on which one you use (React, Preact, Vue, etc.)

If you use more modern JavaScript features than what your users support, install and configure @rollup/plugin-babel.

See also ¶ Vite and ¶ WMR, if you’re using Rollup through them, for more info.

Webpack
Expand example
webpack.config.js
module.exports = {
  module: {
    // …
    rules: [
      // …
      {
        test: /\.mdx?$/,
        use: [
          // `babel-loader` is optional:
          {loader: 'babel-loader', options: {}},
          {
            loader: '@mdx-js/loader',
            /** @type {import('@mdx-js/loader').Options} */
            options: {/* jsxImportSource: …, otherOptions… */}
          }
        ]
      }
    ]
  }
}

We support webpack. Install and configure the webpack loader @mdx-js/loader. Configure your JSX runtime depending on which one you use (React, Preact, Vue, etc.)

If you use more modern JavaScript features than what your users support, install and configure babel-loader.

See also ¶ Create React App (CRA), ¶ Next.js, and ¶ Vue CLI, if you’re using webpack through them, for more info.

Build systems

Snowpack

Snowpack has their own plugin to support MDX. See snowpack-plugin-mdx on how to use MDX with Snowpack.

Vite
Expand example
vite.config.js
import {defineConfig} from 'vite'
import mdx from '@mdx-js/rollup'

export default defineConfig({
  plugins: [
    mdx(/* jsxImportSource: …, otherOptions… */)
  ]
})

Vite supports Rollup plugins directly in plugins in your vite.config.js.

Install and configure the Rollup plugin @mdx-js/rollup.

If you use more modern JavaScript features than what your users support, configure Vite’s build.target.

See also ¶ Rollup, which is used in Vite, and see ¶ Vue, if you’re using that, for more info.

Vue CLI
Expand example
vue.config.js
module.exports = {
  chainWebpack: config => {
    config.module
      .rule('mdx')
      .test(/\.mdx?$/)
      .use('babel-loader')
        .loader('babel-loader')
        .options({plugins: ['@vue/babel-plugin-jsx'], /* Other options… */})
        .end()
      .use('@mdx-js/loader')
        .loader('@mdx-js/loader')
        .options({jsx: true, /* otherOptions… */})
        .end()
  }
}

Vue CLI, in the beta for version 5, supports webpack loaders directly in configureWebpack.plugins in vue.config.js.

Install and configure the webpack loader @mdx-js/loader. You have to configure Vue and Babel too.

See also ¶ webpack, which is used in Vue CLI, and see ¶ Vue, which you’re likely using, for more info.

Note: to support ESM in vue.config.js or vue.config.mjs, you currently have to use their v5.0.0-rc. See v5.0.0-beta.0 in their changelog for more info. Their latest beta release is currently v5.0.0-rc.2.

WMR
Expand example
wmr.config.mjs
import {defineConfig} from 'wmr'
import mdx from '@mdx-js/rollup'

export default defineConfig({
  plugins: [
    mdx({/* jsxImportSource: …, otherOptions… */})
  ]
})

WMR supports Rollup plugins directly by adding them to plugins in wmr.config.mjs.

Install and configure the Rollup plugin @mdx-js/rollup.

See also ¶ Rollup, which is used in WMR, and see ¶ Preact, if you’re using that, for more info.

Compilers

Babel
Expand plugin and sample use

This plugin:

plugin.js
import path from 'node:path'
import parser from '@babel/parser'
import estreeToBabel from 'estree-to-babel'
import {compileSync} from '@mdx-js/mdx'

export function babelPluginSyntaxMdx() {
  // Tell Babel to use a different parser.
  return {parserOverride: babelParserWithMdx}
}

// A Babel parser that parses MDX files with `@mdx-js/mdx` and passes any
// other things through to the normal Babel parser.
function babelParserWithMdx(value, options) {
  if (
    options.sourceFilename &&
    /\.mdx?$/.test(path.extname(options.sourceFilename))
  ) {
    // Babel does not support async parsers, unfortunately.
    return compileSync(
      {value, path: options.sourceFilename},
      // Tell `@mdx-js/mdx` to return a Babel tree instead of serialized JS.
      {recmaPlugins: [recmaBabel], /* jsxImportSource: …, otherOptions… */}
    ).result
  }

  return parser.parse(value, options)
}

// A “recma” plugin is a unified plugin that runs on the estree (used by
// `@mdx-js/mdx` and much of the JS ecosystem but not Babel).
// This plugin defines `'estree-to-babel'` as the compiler, which means that
// the resulting Babel tree is given back by `compileSync`.
function recmaBabel() {
  Object.assign(this, {Compiler: estreeToBabel})
}

Can be used like so with the Babel API:

example.js
import babel from '@babel/core'
import {babelPluginSyntaxMdx} from './plugin.js'

// Note that a filename must be set for our plugin to know it’s MDX instead of JS.
await babel.transformAsync(file, {filename: 'example.mdx', plugins: [babelPluginSyntaxMdx]})

You should probably use webpack or Rollup instead of Babel directly as that gives the neatest interface. It is possible to use @mdx-js/mdx in Babel and it’s fast, because it skips @mdx-js/mdx serialization and Babel parsing, if Babel is used anyway.

Babel does not support syntax extensions to its parser (it has “syntax” plugins but those in fact turn certain flags on or off). It does support setting a different parser. Which in turn lets us choose whether to use the @mdx-js/mdx or @babel/parser.

Site generators

Create React App (CRA)

Note: rewiring with CRACO (see below) is currently required for CRA 5, due to a bug in react-scripts (facebook/create-react-app#12166), which is also tracked at mdx-js/mdx#1870.

Expand example
src/content.mdx
# Hello, world!

This is **markdown** with <span style={{color: "red"}}>JSX</span>: MDX!
src/App.jsx
/* eslint-disable import/no-webpack-loader-syntax */
import Content from '!@mdx-js/loader!./content.mdx'

export default function App() {
  return <Content />
}

CRA supports webpack loaders through webpack loader syntax in imports.

Install the webpack loader @mdx-js/loader.

Expand CRACO example
craco.config.js
const {addAfterLoader, loaderByName} = require('@craco/craco')

module.exports = {
  webpack: {
    configure(webpackConfig) {
      addAfterLoader(webpackConfig, loaderByName('babel-loader'), {
        test: /\.mdx?$/,
        loader: require.resolve('@mdx-js/loader')
      })
      return webpackConfig
    }
  }
}
src/App.jsx
import Content from './content.mdx'

export default function App() {
  return <Content />
}

For importing MDX without the !@mdx-js/loader! prefix, you can add the loader to the webpack config, by rewiring react-scripts using CRACO.

Note: warnings about CRACO having incorrect peer dependency "react-scripts@^4.0.0" can be ignored for the above to work.

See also ¶ Webpack, which is used in CRA, and see ¶ React, which you’re likely using, for more info.

Docusaurus

Docusaurus supports MDX by default. See MDX and React on their website for more on how to use MDX with Docusaurus.

Gatsby

Gatsby has their own plugin to support MDX. See gatsby-plugin-mdx on how to use MDX with Gatsby.

Next.js
Expand example
next.config.js
module.exports = {
  // Prefer loading of ES Modules over CommonJS
  experimental: {esmExternals: true},
  // Support MDX files as pages:
  pageExtensions: ['md', 'mdx', 'tsx', 'ts', 'jsx', 'js'],
  // Support loading `.md`, `.mdx`:
  webpack(config, options) {
    config.module.rules.push({
      test: /\.mdx?$/,
      use: [
        // The default `babel-loader` used by Next:
        options.defaultLoaders.babel,
        {
          loader: '@mdx-js/loader',
          /** @type {import('@mdx-js/loader').Options} */
          options: {/* jsxImportSource: …, otherOptions… */}
        }
      ]
    })

    return config
  }
}

Next supports webpack loaders by overwriting the webpack config in next.config.js.

Install and configure the webpack loader @mdx-js/loader. There is no need to configure your JSX runtime as React is already set up.

See also ¶ Webpack and ¶ React, which you’re using, for more on those tools.

Parcel

Parcel 2 has their own plugin to support MDX. See @parcel/transformer-mdx on how to use MDX with Parcel.

Razzle

Razzle has their own plugin to support MDX. See razzle-plugin-mdx on how to use MDX with Razzle.

React Static

React Static has their own plugin to support MDX. See react-static-plugin-mdx on how to use MDX with React Static.

JSX runtimes

Emotion
Expand example
example.js
import {compile} from '@mdx-js/mdx'

main()

async function main() {
  const js = String(await compile('# hi', {jsxImportSource: '@emotion/react', /* otherOptions… */}))
}

Emotion is supported when options.jsxImportSource is set to '@emotion/react'. You can optionally install and configure @mdx-js/react, which allows for context based component passing.

See also ¶ React, which is used in Emotion, and see ¶ Rollup and ¶ webpack, if you’re using them, for more info.

Ink
Expand example
example.mdx
# Hi!
example.js
import React from 'react'
import {render, Text} from 'ink'
import Content from './example.mdx' // Assumes an integration is used to compile MDX -> JS.

const components = {
  h1(props) {
    return React.createElement(Text, {bold: true, ...props})
  },
  p: Text
}

render(React.createElement(Content, {components}))

Can be used with:

Shell
node --experimental-loader=@mdx-js/node-loader example.js

Ink uses the React JSX runtime, so set that up. You will also want to swap HTML elements out for Ink’s components. See § Table of components for what those are and Ink’s documentation on what you can replace them with.

See also ¶ React and ¶ Node.js, which you’re using, for more info.

Preact
Expand example
example.js
import {compile} from '@mdx-js/mdx'

main()

async function main() {
  const js = String(await compile('# hi', {jsxImportSource: 'preact', /* otherOptions… */}))
}

Preact is supported when options.jsxImportSource is set to 'preact'. You can optionally install and configure @mdx-js/preact, which allows for context based component passing.

See also ¶ esbuild, ¶ Rollup, and ¶ webpack, which you might be using, for more info.

React

React is supported right out of the box. You can optionally install and configure @mdx-js/react, which allows for context based component passing.

See also ¶ esbuild, ¶ Rollup, and ¶ webpack, which you might be using, for more info.

Experiment: while currently in alpha and not shipping soon, React server components will work with MDX too. There is an experimental demo. And our website is made with them!

Theme UI
Expand example

Example w/o @mdx-js/react

example.js
import {base} from '@theme-ui/preset-base'
import {components, ThemeProvider} from 'theme-ui'
import Post from './post.mdx' // Assumes an integration is used to compile MDX -> JS.

<ThemeProvider theme={base}>
  <Post components={components} />
</ThemeProvider>

Example w/ @mdx-js/react

example.js
import {base} from '@theme-ui/preset-base'
import {ThemeProvider} from 'theme-ui'
import Post from './post.mdx' // Assumes an integration is used to compile MDX -> JS.

<ThemeProvider theme={base}>
  <Post />
</ThemeProvider>

Theme UI is a React-specific library that requires using context to access its effective components. You can optionally install and configure @mdx-js/react, which allows for context based component passing.

See also ¶ Emotion, ¶ React, ¶ esbuild, ¶ Rollup, and ¶ webpack, which you might be using, for more info.

Svelte
Expand example
example.js
import {compile} from '@mdx-js/mdx'

main()

async function main() {
  const js = String(await compile('# hi', {jsxImportSource: 'svelte-jsx', /* otherOptions… */}))
}

Svelte is supported when options.jsxImportSource is set to 'svelte-jsx', which is a small package that adds support for the JSX automatic runtime to Svelte.

See also ¶ esbuild, ¶ Rollup, and ¶ webpack, which you might be using, for more info.

Vue
Expand example
example.js
import {compile} from '@mdx-js/mdx'
import babel from '@babel/core'

main()

async function main() {
  const jsx = String(await compile('# hi', {jsx: true, /* otherOptions… */}))
  const js = (await babel.transformAsync(jsx, {plugins: ['@vue/babel-plugin-jsx']})).code
}

Vue 3 is supported when using their custom Babel JSX transformer (@vue/babel-plugin-jsx) and configuring @mdx-js/mdx, @mdx-js/rollup, or @mdx-js/loader with jsx: true. You can optionally install and configure @mdx-js/vue, which allows for context based component passing.

See also ¶ Vite and ¶ Vue CLI, which you might be using, for more info.

JavaScript engines

Node.js

MDX files can be imported in Node by using @mdx-js/node-loader (strongly recommended) or alternatively they can be required with the legacy package @mdx-js/register. See their readmes on how to configure them.

Further reading