A custom Nextra theme is simply a React component that wraps every MDX page on your site. Instead of installingDocumentation 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.
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.
Step-by-Step Setup
A custom theme still needs Nextra itself (for the compiler and page map), but not any theme package:
import nextra from 'nextra'
const withNextra = nextra({})
export default withNextra({
reactStrictMode: true,
})
With a custom theme you rarely need any Nextra-specific options at this level — the defaults work for most setups.
app/layout.tsx is the standard Next.js App Router root layout. It fetches the page map and passes it to your theme component: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
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.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: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,
})
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.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: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 />
</>
)
}
Because
NextraTheme is a Server Component, pageMap is passed straight through without any serialisation overhead.The
normalizePages utility from nextra/normalize-pages transforms the raw page map into structured navigation data. Use topLevelNavbarItems for the top navigation:'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>
)
}
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.Use
docsDirectories from normalizePages for the sidebar. It contains the hierarchical list of pages and folders under the current docs section:'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>
)
}
# Welcome
This page is rendered inside your custom Nextra theme.
The navbar and sidebar are built from the page map automatically.
Start the dev server and navigate to http://localhost:3000:
Key APIs for Custom Themes
getPageMap()
getPageMap()
PageMapItem objects describing every page and section in your content directory. Pass it as a prop to whichever components need to render navigation.normalizePages()
normalizePages()
PageMapItem[] array into structured, route-aware navigation data. It is the primary way to build navbars and sidebars in a custom theme.useMDXComponents wrapper slot
useMDXComponents wrapper slot
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.Anchor component
Anchor component
<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:Styling Your Theme
Nextra imposes no CSS framework or styling conventions on a custom theme. You can use whichever approach fits your project:- CSS Modules
- Tailwind CSS
- Inline styles
usePathname, useRouter, and useSearchParams all work as normal inside your theme components. Mark any component that uses them with 'use client'.
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.