✳ OMNIVIEWER MDX viewer Markdown viewer Audit a 20 GB MDX file ← back

What is an MDX file? Markdown with components, explained

You cloned a docs repo and the pages are .mdx, not .md. Your editor shows Markdown with stray <Callout> tags and import lines at the top, and GitHub renders it half-wrong. MDX is Markdown that can import and render components — one file holding four languages at once. Here is what is actually inside one, with a complete example, what it compiles to, and how to read one in your browser without installing a thing.

or drop an .mdx file here — it opens in your browser, nothing is uploaded
On this page What is MDX A complete file The four layers Frontmatter Imports & exports Components Expressions What it compiles to MDX vs Markdown vs HTML The mistakes everybody makes Opening one without a build

What is MDX?

MDX is a document format that adds two things to Markdown: JSX — component tags like <Callout type="warning"> written inline with your prose — and ESM, the JavaScript import and export statements that make those components available. The file extension is .mdx, it is plain UTF-8 text, and it compiles to a JavaScript module rather than to HTML.

It was created by John Otander, Tim Neutkens, Guillermo Rauch and Brent Jackson, and released in 2018, to end a choice documentation authors kept having to make: prose that is pleasant to write, or pages that can show a live, interactive demo. MDX gives you both in one file. MDX 2 (2022) rewrote the compiler on top of micromark and promoted JSX and curly-brace expressions to real syntax instead of passed-through HTML; MDX 3 (2023) is a smaller step on the same design.

If you have written documentation in the last few years, you have probably written MDX without choosing it. It is the page format behind Docusaurus, Next.js, Astro, Storybook docs pages, Gatsby, Nextra and Redwood.

The smallest interesting MDX file is three lines:

import Chart from './Chart'

# Revenue

<Chart data={[3, 7, 12]} />

Everything that isn't a tag or an import is ordinary Markdown, and behaves exactly as it would in a .md file. That is the whole pitch: you pay the JavaScript tax only on the lines that need it.

A complete file, annotated

Here is a realistic docs page using every part of the format. It is a trimmed version of the sample you can open in the viewer from the box above.

---                                      ← 1. frontmatter (YAML)
title: Shipping your first release
sidebar_position: 3
tags: [guide, releases, ci]
---

import Callout from '@site/src/components/Callout'     ← 2. ESM
import { Tabs, TabItem } from '@theme/Tabs'
export const releasedOn = '2026-08-04'

# Shipping your first release                          ← 3. Markdown

Cutting a release takes one command. Last shipped: {releasedOn}.

<Callout type="info" title="Before you start">          ← 4. JSX

You need push rights and an `NPM_TOKEN` in the repo secrets.

</Callout>

## The one command

<Tabs>
  <TabItem value="npm" label="npm">

    ```bash
    npm version minor && git push --follow-tags
    ```

  </TabItem>
</Tabs>

```js
// inside a fence this is sample text, not a real import
import NotReal from './nope'
```

Read that last block carefully, because it is the thing every naive MDX tool gets wrong. The import on the final line is inside a fenced code block, so it is Markdown content — not an ESM statement. Anything that greps for ^import will hallucinate a dependency that doesn't exist, and anything that greps for <Tag> will report components from your code samples.

The four layers in one file

Every byte of an MDX document belongs to exactly one of four layers. Knowing which one is what makes MDX tooling harder than Markdown tooling:

LayerLooks likeWhere it's allowed
Frontmattera ----fenced YAML blockThe very top of the file, once
ESMimport X from '…', export const y = …Column 0 of the document body only
JSX<Callout type="warn">…</Callout>Anywhere a Markdown block or inline can go
Expressions{2 + 2}, {/* a comment */}Anywhere, including inside JSX attributes
Markdowneverything elseIncluding inside JSX children

The layers nest. Markdown can appear inside a JSX element's children, which can contain more JSX, which can contain an expression holding a JavaScript object. That recursion is why MDX needs a real parser and not a regular expression — and why the viewer's COMPONENTS tab exists.

Frontmatter: the metadata block

Most MDX pipelines let you open a file with a ----fenced YAML block. It is not part of the MDX language itself — it comes from a plugin (remark-frontmatter, plus remark-mdx-frontmatter to turn it into exports), and every framework wires it up by default:

---
title: Shipping your first release
description: From tag to changelog.
sidebar_position: 3
draft: false
---

The keys are pure convention — title, tags and draft mean whatever your site generator says they mean. Docusaurus reads sidebar_position; Astro reads whatever your content-collection schema declares. If you're chasing a page that sorts wrong or won't publish, the frontmatter is the first thing to check, and the viewer surfaces it as a labelled block at the top of the PREVIEW tab.

Imports and exports

An MDX file is a JavaScript module, so it can do what modules do. import pulls in components (or data, or styles); export publishes values that the file itself can use and that the importing page can read:

import Callout from '@site/src/components/Callout'
import { Tabs, TabItem } from '@theme/Tabs'
import data from './benchmarks.json'

export const releasedOn = '2026-08-04'
export const meta = { reviewed: true, owner: 'platform' }

Two rules that trip people up. First, these statements must start at column 0 — indent one and it becomes a Markdown paragraph (or, indented four spaces, a code block). Second, they can be anywhere in the document, not just the top; an import halfway down the page is legal and hoisted, though hardly kind to the next reader.

Components: the capital letter that changes everything

This is the single most important rule in MDX, and it is decided entirely by the first character of the tag name:

A component resolves from exactly three places, in order: an import in the file, something the file exports, or the components object supplied at render time by an MDXProvider. Nothing else. Props are JSX props — a quoted string, or any JavaScript inside braces:

<ReleaseTimeline
  stages={['tag', 'build', 'sign', 'publish']}
  live
  caption="What CI does after the tag"
/>

Children are where MDX earns its name: put blank lines around the content and it is parsed as Markdown. Leave them out and it is treated as JSX text, so your **bold** arrives on the page as literal asterisks.

<Callout type="warning">

Publishing is **not** reversible.        ← Markdown: renders bold

</Callout>

<Callout type="warning">Publishing is **not** reversible.</Callout>
                                          ← JSX text: renders **not**

Expressions

A pair of braces drops you into JavaScript, anywhere in the document:

Last release: {releasedOn}, {new Date().getFullYear()} edition.

{/* This is how you write a comment in MDX. HTML comments are not allowed. */}

<Chart data={data.rows.filter(r => r.visible)} />

The expression is evaluated when the page renders, so it can reference anything in scope: an import, an export from the same file, or a prop. The consequence — and the reason MDX is a build-time format rather than a document format — is that an MDX file is code. You cannot render one you don't trust any more than you'd run a script you don't trust.

What it compiles to

MDX doesn't produce HTML. It produces a JavaScript module exporting a component. That earlier three-line example compiles, roughly, to this:

import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from 'react/jsx-runtime'
import Chart from './Chart'

function _createMdxContent(props) {
  const _components = { h1: 'h1', ...props.components }       // overridable
  return _jsxs(_Fragment, { children: [
    _jsx(_components.h1, { children: 'Revenue' }),            // # Revenue
    _jsx(Chart, { data: [3, 7, 12] })                         // <Chart …/>
  ]})
}

export default function MDXContent(props = {}) {
  const { wrapper: MDXLayout } = { ...props.components }
  return MDXLayout
    ? _jsx(MDXLayout, { ...props, children: _jsx(_createMdxContent, props) })
    : _createMdxContent(props)
}

Three details in there explain most of MDX's behaviour. The _components object is why an MDXProvider can replace every h1 on your site with your own heading component. Chart is a bare identifier — which is why a missing import is a runtime failure, not a compile one. And because the output is a component, an MDX page composes like any other component: props in, JSX out.

The error everybody meets: Expected component `Kbd` to be defined: you likely forgot to import, pass, or provide it. Nothing checks that a capitalised tag resolves until the compiled module is rendered — typically in the middle of a build of two thousand pages, with a stack trace that names no file. The viewer's COMPONENTS tab lists every tag in a file against its imports and exports up front, so you find the one you forgot before CI does.

MDX vs Markdown vs HTML

AspectMDXMarkdownHTML
Extension.mdx.md.html
ComponentsYes — real JSXNoWeb components only
Needs a build stepYesNoNo
Renders on GitHubPartially — tags leakYesAs source
Compiles toA JS moduleHTMLItself
Safe to render untrustedNo — it's codeWith sanitisingWith sanitising
Raw HTML in the fileParsed as JSX (className)Passed through (class)n/a

The row that matters most in practice is the last one. In a .md file, HTML is opaque text handed to the browser. In MDX there is no raw HTML — what looks like HTML is JSX, and it plays by JSX rules. That single difference is behind most of the surprises below. If your file has no components in it, the Markdown viewer is the simpler tool; if it does, use the MDX one.

The mistakes everybody makes

Opening an MDX file without a build step

The awkward part of MDX is that reading one properly normally means running a toolchain: install the framework, resolve every component, start a dev server. If all you want is to see what a file says and what it depends on, that is a lot of ceremony — and it's impossible for a file whose components live in a repo you don't have.

The OmniViewer MDX toolkit takes the other approach: parse the document, and draw every component you can't resolve as a labelled placeholder card carrying its props and its rendered children. You see the prose properly formatted, and you see exactly where the components sit, without a single one of them being installed — or executed. Nothing is uploaded, and no JavaScript from the file ever runs.

The component audit streams the whole file rather than a bounded prefix, so it holds up on documents far larger than memory — a generated API reference or a decade of concatenated changelogs. That mechanism, and the resumable scanner behind it, is the subject of the companion piece: how to audit a 20 GB MDX file.

Open the MDX viewer →Preview the document, audit every component and import, tidy the source — locally, no upload. The Markdown viewer →For plain .md: preview, formatted source, HTML output and contents. Audit a 20 GB MDX file →Why the compiler can't scale, and the resumable scanner that can — in real code.

OmniViewer opens every file format in your browser — Markdown, JSON, YAML, JavaScript, CSV, HTML and more — powered by the same windowed engine as fastjsonviewer.com and hugecsv.com. MDX is one of the formats with dedicated tooling.