Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/shuding/nextra/llms.txt

Use this file to discover all available pages before exploring further.

A custom Nextra theme is simply a React component that wraps every MDX page on your site. Instead of installing nextra-theme-docs or nextra-theme-blog, you write the layout yourself — giving you complete control over structure, styling, and behaviour. Nextra still handles MDX compilation, file-system routing, and the page map; your theme decides how to present that data.
The full source code for the custom theme example is available at examples/custom-theme in the Nextra repository.

How a Custom Theme Works

Nextra compiles every .md and .mdx file into a React component and passes its rendered output as children to your theme layout. The theme receives the content and can surround it with any navigation, sidebar, header, footer, or TOC it needs. The page map — a serialisable tree of every page and section in your site — is fetched at build time via getPageMap() and passed down to whichever components need it.
MDX file → Nextra compiler → React component (children)

             Root layout → Theme component → {navbar, sidebar, children, footer}

Step-by-Step Setup

1
Install dependencies
2
A custom theme still needs Nextra itself (for the compiler and page map), but not any theme package:
3
npm
npm install next react react-dom nextra
yarn
yarn add next react react-dom nextra
pnpm
pnpm add next react react-dom nextra
bun
bun add next react react-dom nextra
4
Configure the Nextra plugin
5
Create next.config.ts (or .mjs) and wrap your Next.js config:
6
import nextra from 'nextra'

const withNextra = nextra({})

export default withNextra({
  reactStrictMode: true,
})
7
With a custom theme you rarely need any Nextra-specific options at this level — the defaults work for most setups.
8
Create the root layout
9
app/layout.tsx is the standard Next.js App Router root layout. It fetches the page map and passes it to your theme component:
10
import type { Metadata } from 'next'
import { Head } from 'nextra/components'
import { getPageMap } from 'nextra/page-map'
import type { FC, ReactNode } from 'react'
import { NextraTheme } from './_components/nextra-theme'

export const metadata: Metadata = {
  title: {
    absolute: '',
    template: '%s - My Site',
  },
}

const RootLayout: FC<{ children: ReactNode }> = async ({ children }) => {
  const pageMap = await getPageMap()
  return (
    <html lang="en" dir="ltr">
      <Head faviconGlyph="✦" />
      <body style={{ margin: 0 }}>
        <NextraTheme pageMap={pageMap}>{children}</NextraTheme>
      </body>
    </html>
  )
}

export default RootLayout
11
Fetching pageMap in the root layout and passing it as a prop is the recommended pattern. This keeps <NextraTheme> a Server Component by default, avoiding unnecessary client-side JavaScript.
12
Create the MDX components file
13
Create mdx-components.jsx at the project root. For a custom theme, import from nextra/mdx-components and use the wrapper slot to inject a table of contents alongside the content:
14
import { useMDXComponents as getNextraComponents } from 'nextra/mdx-components'
import { TOC } from './app/_components/toc'

const defaultComponents = getNextraComponents({
  wrapper({ children, toc }) {
    return (
      <>
        <div style={{ flexGrow: 1, padding: 20 }}>{children}</div>
        <TOC toc={toc} />
      </>
    )
  },
})

export const useMDXComponents = (components) => ({
  ...defaultComponents,
  ...components,
})
15
The toc prop is an array of Heading objects ({ id, value, depth }). The wrapper function receives this alongside children so you can render a table of contents without any extra data-fetching.
16
Build the theme component
17
Create app/_components/nextra-theme.tsx. This is the heart of your custom theme — it accepts children (the compiled MDX content) and pageMap, then assembles the full page shell:
18
import type { PageMapItem } from 'nextra'
import type { FC, ReactNode } from 'react'
import { Footer } from './footer'
import { Navbar } from './navbar'
import { Sidebar } from './sidebar'

export const NextraTheme: FC<{
  children: ReactNode
  pageMap: PageMapItem[]
}> = ({ children, pageMap }) => {
  return (
    <>
      <Navbar pageMap={pageMap} />
      <div style={{ display: 'flex' }}>
        <Sidebar pageMap={pageMap} />
        {children}
      </div>
      <Footer />
    </>
  )
}
19
Because NextraTheme is a Server Component, pageMap is passed straight through without any serialisation overhead.
20
Build the Navbar with normalizePages
21
The normalizePages utility from nextra/normalize-pages transforms the raw page map into structured navigation data. Use topLevelNavbarItems for the top navigation:
22
'use client'

import { usePathname } from 'next/navigation'
import type { PageMapItem } from 'nextra'
import { Anchor } from 'nextra/components'
import { normalizePages } from 'nextra/normalize-pages'
import type { FC } from 'react'

export const Navbar: FC<{ pageMap: PageMapItem[] }> = ({ pageMap }) => {
  const pathname = usePathname()
  const { topLevelNavbarItems } = normalizePages({
    list: pageMap,
    route: pathname,
  })

  return (
    <nav style={{ display: 'flex', gap: 20, padding: 20 }}>
      {topLevelNavbarItems.map((item) => {
        const href = item.route ?? ('href' in item ? item.href : '')
        return (
          <Anchor key={href} href={href}>
            {item.title}
          </Anchor>
        )
      })}
    </nav>
  )
}
23
Mark navbar and sidebar components with 'use client' because they call usePathname() to highlight the active route. The theme component itself can stay as a Server Component.
24
Build the Sidebar with normalizePages
25
Use docsDirectories from normalizePages for the sidebar. It contains the hierarchical list of pages and folders under the current docs section:
26
'use client'

import { usePathname } from 'next/navigation'
import type { PageMapItem } from 'nextra'
import { Anchor } from 'nextra/components'
import { normalizePages } from 'nextra/normalize-pages'
import type { FC } from 'react'

export const Sidebar: FC<{ pageMap: PageMapItem[] }> = ({ pageMap }) => {
  const pathname = usePathname()
  const { docsDirectories } = normalizePages({
    list: pageMap,
    route: pathname,
  })

  return (
    <aside style={{ width: 240, padding: 20 }}>
      <ul style={{ listStyle: 'none', padding: 0 }}>
        {docsDirectories.map(function renderItem(item) {
          const href = item.route ?? ('href' in item ? item.href : '')
          return (
            <li key={href}>
              {'children' in item ? (
                <details>
                  <summary>{item.title}</summary>
                  {item.children.map((child) => renderItem(child))}
                </details>
              ) : (
                <Anchor href={href}>{item.title}</Anchor>
              )}
            </li>
          )
        })}
      </ul>
    </aside>
  )
}
27
Add your first MDX page
28
Create app/page.mdx (the site root) or any nested app/.../page.mdx:
29
# Welcome

This page is rendered inside your custom Nextra theme.
The navbar and sidebar are built from the page map automatically.
30
Start the dev server and navigate to http://localhost:3000:
31
npm
npm run dev
yarn
yarn dev
pnpm
pnpm dev
bun
bun dev

Key APIs for Custom Themes

import { getPageMap } from 'nextra/page-map'

const pageMap = await getPageMap()
// Returns PageMapItem[] — a tree of pages, folders, and meta files
Call this in any async Server Component (typically the root layout). The result is a serialisable array of PageMapItem objects describing every page and section in your content directory. Pass it as a prop to whichever components need to render navigation.
import { normalizePages } from 'nextra/normalize-pages'

const {
  topLevelNavbarItems, // Items for a top navigation bar
  docsDirectories,     // Hierarchical sidebar items
  activePath,          // Breadcrumb trail to the current page
  activeType,          // 'page' | 'doc' | 'menu' ...
} = normalizePages({
  list: pageMap,
  route: pathname,     // Current URL pathname
})
This utility transforms the raw PageMapItem[] array into structured, route-aware navigation data. It is the primary way to build navbars and sidebars in a custom theme.
import { useMDXComponents as getNextraComponents } from 'nextra/mdx-components'

const components = getNextraComponents({
  // The wrapper receives children (MDX content) and toc (headings array)
  wrapper({ children, toc }) {
    return (
      <>
        <main>{children}</main>
        <aside>
          {toc.map(h => <a key={h.id} href={`#${h.id}`}>{h.value}</a>)}
        </aside>
      </>
    )
  },
})
The wrapper function is called for every MDX page. The toc prop is an array of Heading objects ({ id: string, value: string, depth: number }) extracted from the page’s headings — use it to render a table of contents without any extra data fetching.
import { Anchor } from 'nextra/components'

<Anchor href="/docs/intro">Introduction</Anchor>
A lightweight wrapper around Next.js <Link> that handles both internal and external URLs correctly. Use it in navbar and sidebar items instead of plain <a> tags.

Project Structure

A minimal custom theme project looks like this:
my-custom-theme/
├── app/
│   ├── _components/
│   │   ├── nextra-theme.tsx  # Root theme wrapper component
│   │   ├── navbar.tsx        # Top nav — uses topLevelNavbarItems
│   │   ├── sidebar.tsx       # Side nav — uses docsDirectories
│   │   ├── footer.tsx        # Footer
│   │   └── toc.tsx           # Table of contents
│   ├── docs/
│   │   └── intro/
│   │       └── page.mdx      # A docs page at /docs/intro
│   ├── layout.tsx            # Root layout — fetches pageMap, renders <NextraTheme>
│   └── page.mdx              # Home page at /
├── mdx-components.jsx        # MDX component overrides + wrapper slot
├── next.config.ts            # Nextra plugin config
└── package.json

Styling Your Theme

Nextra imposes no CSS framework or styling conventions on a custom theme. You can use whichever approach fits your project:
import styles from './navbar.module.css'

export const Navbar = ({ pageMap }) => (
  <nav className={styles.navbar}>...</nav>
)
Next.js hooks like usePathname, useRouter, and useSearchParams all work as normal inside your theme components. Mark any component that uses them with 'use client'.
Do not call usePathname() or other client hooks in Server Components. Keep the root layout and theme wrapper as Server Components and push client interactivity into the smallest possible leaf components (navbar, sidebar, TOC).

Next Steps

Docs Theme

Use the official docs theme for a production-ready setup with zero boilerplate.

Blog Theme

Use the blog theme for post listings, tags, RSS, and reading-time estimates.

Custom Theme Example

Browse the complete working example on GitHub.

normalizePages API

Learn more about the page map structure and how normalizePages works.

Build docs developers (and LLMs) love