Writing Interactive Documents in Markdown with MDX

Alex Krolick
3 min readSep 2, 2018
Photo by Ricardo Gomez Angel on Unsplash
Example of an interactive MDX document linking a dataset to a table, form, and dynamic values displayed in a text and chart: https://mdx-observable.netlify.com/dataviz

MDX

MDX is a format that lets you use React components in Markdown files.

  • Markdown is a plaintext format that compiles to HTML, the native document format of the web
  • React is a UI framework that makes it easy to build data-driven, interactive UIs in HTML and JavaScript

MDX is a great format for writing documents like scientific papers, data visualizations, and presentations because easy-to-write Markdown syntax can be combined with JavaScript-powered data tables, maps, graphs, and API calls.

MDX files can also be compiled into standalone webpages.

A typical .mdx file looks like this:

import { Table } from './table-component';  // React components!
import Chart from './chart-component'; // React components!
import data from './data' // JavaScript data file
# Markdown HeadingSome content<Table data={data} />Here is a chart of the data:<Chart data={data} color="purple" />

MDX documents are static by default

If you want to add interactive features like filters, sliders, or forms, you’ll have to synchronize them somehow. mdx-observable is a library I’m working on to make this easier. The reason you need a special library is because there is no way to create local variables within an MDX document, so you can’t inline the creation of something like an observable or standard React state management utility like Redux.

Note: MDX works great out-of-the-box when all the React components on a page are independent or static, i.e., the table doesn’t affect the chart.

Using MDX-Observable to add state

mdx-observable makes it possible for JSX blocks in MDX to share data. The library connects components using React Context and uses the render proppattern to allow inline JavaScript inside JSX.

The example below adds a piece of data called “count” that gets incremented whenever a button gets clicked. React automatically updates the text with the new count whenever it changes.

import { State, Observe } from "mdx-observable";# Counter<State initialState={{ count: 0 }}><Observe>
{({ setState }) => (
<button onClick={() => setState(s => ({ count: s.count + 1 }))}>
Click me
</button>
)}
</Observe>
<Observe>
{ ({ count }) => (`The button has been clicked ${count} times`) }
</Observe>
</State>

The “reactive programming” model is very powerful because it means the the state of the UI is computed from data. In this case, mdx-observable allows JSX blocks to subscribe to data, and provides a function that components can call to update that data and trigger a re-computation of the entire UI. React is really good at updating only components that changed, so updates appear almost instantly.

Why we need interactive documents

Once the narrative is chosen, thinking stops
- “Narrative Neediness”, Mike Caulfield

The point of adding interactivity to a document is to make it explorable, to allow the reader to tweak parameters, change inputs, change the rules, to let them see what happens, come to their own conclusions, and develop their own intuition.

For more on this topic, see Bret Victor’s interactive essay, “Up and Down the Ladder of Abstraction.”

Other ways to write interactive documents

Most data-centric documents fall into one of these categories:

  • “paper-like” — fully static webpages, Word docs, or PDFs with embedded figures like images and maps
  • videos
  • scripts that run in desktop software like Matlab, Octave, R, or Mathematica
  • hand-coded one-off applications (web, mobile, or desktop)
  • “notebooks” that run in Jupyter or Observable

These options tend to be be:

  • hard to write (complex tooling or manual build steps)
  • hard to collaborate (poor interoperability with version control or file sharing)
  • hard to consume (require special software to run)

MDX is much better on these fronts.

--

--