Skip to navigation
5-8 minutes read
By Titus Wormer

Note: This is an old blog post. The below is kept as is for historical purposes.

Note: Info on how to migrate is available in our Version 2 migration guide.

MDX 2

Version 2 of MDX was released after years of hard work, and has many improvements. Here are the highlights:

  • 📝 Improved syntax makes it easier to use markdown in JSX
  • 🧑‍💻 JavaScript expressions turn {2 * Math.PI} into 6.283185307179586
  • 🔌 New esbuild, Rollup, and Node.js integrations
  • ⚛️ Any JSX runtime: React, Preact, Vue, Emotion, you name it, they’re all supported
  • 🌳 Improved AST exposes more info in greater detail
  • 🏃‍♀️ Compiles at least 25% faster
  • 🚴 Generated code runs twice as fast (100% faster)
  • 🚄 Bundle size of @mdx-js/mdx is more than three times as small (250% smaller)
  • 🧵 …and much, so much more

It’s been 4 years since this project was announced. 2½ since we released our stable version 1. We’re proud to finally release v2! Let’s dive in!

Contents

Improvements to the MDX format

We’ve spent a lot of time thinking and trying out different ways to improve the format. Originally, the format was very close to how markdown, and HTML in markdown, works. We found that the old behavior often yielded unexpected results. In version 2, we shift the format a little bit closer to how JS(X) works.

Take for example this MDX file:

<div>*hi*?</div>

<div>
  # hi?
</div>

<main>
  <div>

    # hi?

  </div>
</main>

In version 1, that was equivalent to the following JSX:

<>
  <div>*hi*?</div>
  <div>
    # hi?
  </div>
  <main>
    <div>
      <pre><code># hi?</code></pre>
    </div>
  </main>
</>

In version 2, it’s equivalent to the following JSX:

<>
  <div><em>hi</em>?</div>
  <div>
    <h1>hi?</h1>
  </div>
  <main>
    <div>
      <h1>hi?</h1>
    </div>
  </main>
</>

We believe it’s more intuitive that markdown “inlines” such as emphasis can form between tags on the same line (first <div>), “blocks” such as headings can form if they’re on their own line (second <div>), and indentation is allowed rather than forming code (third <div>).

We also added support for JavaScript expressions, take for example:

expressions.mdx
export const authors = [
  {name: 'Jane', email: 'hi@jane.com'},
  {name: 'John', twitter: '@john2002'}
]
export const published = new Date('2022-02-01')

Written by: {new Intl.ListFormat('en').format(authors.map(d => d.name))}.

Published on: {new Intl.DateTimeFormat('en', {dateStyle: 'long'}).format(published)}.
equivalent.jsx
<>
  <p>Written by: Jane and John.</p>
  <p>Published on: February 1, 2022.</p>
</>

As the format moves a little further from markdown towards JSX, we’ve added support for loading “normal” markdown without our syntax extensions. If you’re using our bundler integration @mdx-js/loader (or @mdx-js/rollup, @mdx-js/esbuild, or @mdx-js/node-loader, see next section), then that’ll just work: import readme.mdx and it’s seen as the MDX format, import readme.md and it’s seen as markdown, based on their extensions.

The MDX format is described in § What is MDX?

Rollup, esbuild, and other integrations

When we started out, Babel, webpack, and React were ubiquitous in the JavaScript ecosystem and we built MDX on them. For version 2, we worked hard to make them optional. They’re no longer required and MDX can more easily be used with other tools.

On the bundler side, we’ve added new integrations: @mdx-js/esbuild for esbuild (an extremely fast bundler) and @mdx-js/rollup for Rollup (a bundler that’s also used in Vite and WMR). You can even import MDX files directly in Node.js with our new @mdx-js/node-loader.

On the runtime side, we can now compile JSX away to normal JavaScript and no longer need framework-specific code to glue them together with MDX. Now any JSX runtime, whether classic or automatic, is supported. This means MDX can be used with React, Preact, Vue, Emotion, Theme UI, Ink, you name it, plus anything that’s yet to be invented!

See § Getting started for a quick start but also in-depth info on how to integrate MDX with many different tools.

Architectural improvements

To make the syntax of the MDX format better and to allow new integrations and arbitrary JSX runtimes, we’ve rewritten almost everything. Part of that effort was micromark, which is another story, but it means we’re fully CommonMark (and optionally GFM) compliant, while also understanding more about MDX files.

Now we can throw an early error when there’s a typo and point to exactly where it happened, instead of a later bundler like webpack complaining about an error in an intermediate, unrecognizable, and broken string of JavaScript. It also means that we have a new AST which describes the MDX syntax in more detail — we even expose the embedded JavaScript. This detailed AST allows plugins to enhance MDX in powerful new ways. It also helped solve edge cases where MDX would previously crash. And it let us drop Babel.

This new architecture results in 25% faster compilation, generated code that runs twice as fast (100% faster), and that the bundle size of @mdx-js/mdx is more than three times as small (250% smaller).

See ¶ Architecture in @mdx-js/mdx for more on how our compiler works. See § Extending MDX for several plugins that use the new tree to enhance MDX, how to use them and other plugins, and how to create plugins.

TypeScript

All @mdx-js/* packages are now fully typed with TypeScript through JSDoc comments. That means our APIs are exposed as TypeScript types but also that our projects are type safe internally.

We’ve published @types/mdx, a types-only package that can be used with any (community) integration. You can use it, if you’re importing MDX files, to type those imported components.

See ¶ TypeScript in § Getting started for more on how to use configure TypeScript.

Docs & site

MDX is used and liked a lot. Before version 1 we had amassed a total of 2.5m downloads. Now we hit that number every week. Our core compiler @mdx-js/mdx is used in 61k projects. Our GitHub repo has 11.6k stars.

Many people help, often with docs. 85 contributors committed to our repo since version 1. We’re grateful for these contributions and all those individual insights, but over the years it did result in some inconsistencies and duplicated content.

For version 2, we rewrote our docs from beginning to end to tell a consistent story for new users, folks that do complex AST and compiler stuff, and anyone in between.

We also made a new website. It’s built on MDX of course, unified itself, and React Server Components (RSC). While we dogfood the former two as they’re projects we maintain, and the latter is extremely experimental, we think compiling things ahead of time and betting on hybrid models, compared to completely server-side sites or completely client-side apps, is the smart choice for us and the web’s future.

Our new site is heavily optimized and fast, gorgeous (subjective but hey), has rich metadata, and scores very well in performance and accessibility review tools such as Lighthouse and axe.

See § Contribute for more on how to help.

Breaking changes

When you’re ready to migrate, please see the Version 2 migration guide.

Thanks

We’d like to say thanks to all our contributors and our happy users. Special thanks to John Otander (@johno), Christian Murphy (@ChristianMurphy), JounQin (@JounQin), Jack Bates (@jablko), Sam Chen (@chenxsan), Sam Robbins (@samrobbins85), Remco Haszing (@remcohaszing), LB (@laurieontech), Gabriel Kirkley (@gdgkirkley>), Hung-I Wang (@Gowee), Ilham Wahabi (@iwgx), Kaito Sugimoto (@HelloRusk), Karl Horky (@karlhorky), Katie Hughes (@glitteringkatie), Lachlan Campbell (@lachlanjc), Marcy Sutton (@marcysutton), Marius Gundersen (@mariusGundersen), Marius-Remus Mate, Mark Skelton (@mskelton), Martin V (@ndresx), Matija Marohnić (@silvenon), Michael Oliver (@michaeloliverx), Michaël De Boey (@MichaelDeBoey), Muescha (@muescha), Norviah (@Norviah), Paul Scanlon (@PaulieScanlon), Peter Mouland (@peter-mouland), Prince Wilson (@maxcell), Rafael Almeida (@rafaelalmeidatk), Rodrez (@rodrez), Rongjian Zhang (@pd4d10), Taeheon Kim (@lonyele), Tom Parker-Shemilt (@palfrey), Try Ajitiono (@imballinst), Yamagishi Kazutoshi (@ykzts), Yoav Kadosh (@ykadosh), Yordis Prieto (@Yordis), Adrian Foong (@adrfoong), Dan Abramov (@gaearon), @ihupoo, @nikhog, @redallen, Akshay Kadam (@deadcoder0904), я котик пур-пур (@mvasilkov), Anders D. Johnson (@AndersDJohnson), Andrew Aylett (@andrewaylett), Ankeet Maini (@ankeetmaini), Biswas Nandamuri (@Biswas-N), Bret (@bcomnes), Chris Chinchilla (@ChrisChinchilla), Christopher Biscardi (@ChristopherBiscardi), Dan Overton (@dan-overton), Domitrius (@domitriusclark), Dovi Winberger (@dowi), Emmie Päivärinta (@emmiep), Eugene Ghanizadeh (@loreanvictor), and anyone we may have forgotten.