mdx-components.js

Maps MDX elements to React components and is required by @next/mdx in the App Router.

Signature
export function useMDXComponents(): MDXComponents
  • Added in Next.js 13.1.2

Return value

MDXComponentsAn object whose keys are MDX/HTML element names and whose values are the React components rendered in their place.

Errors

@next/mdx does not work in the App Router without this file — MDX pages fail to build when it is missing from the project root.

Markdown compiles down to plain HTML elements, so an MDX ## becomes an h2 with no styling and an image becomes a raw img. This file is where you intercept that: return an object keyed by element name and every MDX file in the application renders your component instead. Mapping img to next/image is the most common reason to create it.

When to reach for it

  • You installed @next/mdx — the file is mandatory, not optional.
  • You want global typography or a custom code block across all MDX content.

What trips people up

Despite the use prefix it is not a React hook, and it must be named exactly useMDXComponents. It takes no arguments; older code that wrote useMDXComponents(components) and spread the incoming object was following an earlier signature and now receives nothing.

The file belongs at the project root — beside app, or inside src if you use one — never inside app. Per-page overrides do not go here: pass a components prop to an imported MDX component instead, and it merges over these global ones.

Example

Example · typescript
import type { MDXComponents } from 'mdx/types'
import Image, { type ImageProps } from 'next/image'

// Applies to every .mdx file in the application.
const components = {
  h1: ({ children }) => (
    <h1 style={{ color: 'red', fontSize: '48px' }}>{children}</h1>
  ),
  img: (props) => (
    <Image
      sizes="100vw"
      style={{ width: '100%', height: 'auto' }}
      {...(props as ImageProps)}
    />
  ),
} satisfies MDXComponents

export function useMDXComponents(): MDXComponents {
  return components
}

When to use it

  • Render every image in your MDX posts through next/image.
  • Apply site typography to headings and paragraphs across all markdown content.
  • Swap fenced code blocks for a syntax-highlighted client component.
  • Add anchor links to every heading in a documentation site.

Frequently asked questions

Is mdx-components.tsx required in Next.js?

Yes, when using @next/mdx with the App Router — the setup will not work without it. It must sit at the root of your project, at the same level as app or pages, or inside src if you use one.

Why does useMDXComponents not receive a components argument?

Because the function takes no arguments in current Next.js. Define every component inside the file and return the map; code that destructured or spread an incoming components parameter was written against an earlier signature.

How do I use different MDX components on one page only?

Pass a components prop to the imported MDX component, for example <Welcome components={overrideComponents} />. Those merge with and override the global map returned by useMDXComponents.

More examples

The minimum file that makes @next/mdx work

An empty map is still a required file. Delete it and MDX pages stop building, which is why the usual first symptom of a broken @next/mdx setup is a missing mdx-components file rather than a syntax error in the markdown. Everything renders as default HTML elements until you add keys.

Example · typescript
import type { MDXComponents } from 'mdx/types'

const components: MDXComponents = {}

export function useMDXComponents(): MDXComponents {
  return components
}

Global map plus a per-page override

Two files shown together to make the precedence visible: the components prop on an imported MDX component merges with and overrides the global map, so welcome.mdx renders CustomH1 while every other MDX file keeps the className version. This only works for MDX you import; file-routed page.mdx has no prop to pass.

Example · typescript
// mdx-components.tsx — global
import type { MDXComponents } from 'mdx/types'

const components: MDXComponents = {
  h1: ({ children }) => <h1 className="title">{children}</h1>,
}

export function useMDXComponents(): MDXComponents {
  return components
}

// app/mdx-page/page.tsx — local override
import Welcome from '@/markdown/welcome.mdx'

function CustomH1({ children }) {
  return <h1 style={{ color: 'blue', fontSize: '100px' }}>{children}</h1>
}

export default function Page() {
  return <Welcome components={{ h1: CustomH1 }} />
}

Required next.config.mjs alongside it

mdx-components.tsx alone does nothing: without pageExtensions listing 'md' and 'mdx', a page.mdx file is not treated as a route and returns a 404. By default @next/mdx compiles only .mdx; handling .md as well needs the extension option passed to createMDX.

Example · typescript
import createMDX from '@next/mdx'

/** @type {import('next').NextConfig} */
const nextConfig = {
  // Configure `pageExtensions` to include markdown and MDX files
  pageExtensions: ['js', 'jsx', 'md', 'mdx', 'ts', 'tsx'],
}

const withMDX = createMDX({
  // Add markdown plugins here, as desired
})

// Merge MDX config with Next.js config
export default withMDX(nextConfig)

Discussion

  • Be the first to comment on this lesson.