Skip to Content
🎉 Nextra 4.0 is released. Dima Machina is looking for a new job or consulting.
DocumentationCustom theme

Custom theme

A theme in Nextra works like a layout, that will be rendered as a wrapper for all pages. This docs will walk you through the process of creating a custom theme.

Note

Source code for the following custom theme can be found here.

Create a custom theme

Create a root layout

app/layout.tsx
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 - Nextra' } } 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

Create mdx-components file

mdx-components.jsx
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 })
app/_components/toc.tsx
import type { Heading } from 'nextra' import type { FC } from 'react' export const TOC: FC<{ toc: Heading[] }> = ({ toc }) => { return ( <div style={{ background: 'lightblue', padding: 20 }}> <h3>Table of Contents</h3> <ul> {toc.map(heading => ( <li key={heading.id}>{heading.value}</li> ))} </ul> </div> ) }

Create a basic theme

You can now start working on your theme! Create the nextra-theme.tsx file, it accepts a children prop, which is the MDX content of the current page, and wraps some other elements around the content:

app/_components/nextra-theme.tsx
import type { PageMapItem } from 'nextra' import { version } from 'nextra/package.json' 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 ( <> <h1 style={{ margin: 0, padding: 20, background: 'lightslategray', fontWeight: 'normal' }} > Custom theme demo for <strong>Nextra {version}</strong> </h1> <Navbar pageMap={pageMap} /> <div style={{ display: 'flex' }}> <Sidebar pageMap={pageMap} /> {children} </div> <Footer /> </> ) }
app/_components/footer.tsx
import type { FC } from 'react' export const Footer: FC = () => { return ( <footer style={{ background: 'lightsalmon', padding: 20 }}> Powered by Nextra {new Date().getFullYear()} </footer> ) }
app/_components/navbar.tsx
'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 ( <ul style={{ display: 'flex', listStyleType: 'none', padding: 20, gap: 20, background: 'lightcoral', margin: 0 }} > {topLevelNavbarItems.map(item => { const route = item.route || ('href' in item ? item.href! : '') return ( <li key={route}> <Anchor href={route} style={{ textDecoration: 'none' }}> {item.title} </Anchor> </li> ) })} </ul> ) }

Create sidebar

app/_components/sidebar.tsx
'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 ( <div style={{ background: 'lightgreen', padding: 20 }} > <h3>Sidebar</h3> <ul style={{ margin: 0, display: 'flex', flexDirection: 'column', listStyleType: 'none', padding: 0, gap: 20 }} > {docsDirectories.map(function renderItem(item) { const route = item.route || ('href' in item ? (item.href as string) : '') const { title } = item return ( <li key={route} style={{ padding: '4px 4px 4px 10px', border: '1px solid' }} > {'children' in item ? ( <details> <summary>{title}</summary> {item.children.map(child => renderItem(child))} </details> ) : ( <Anchor href={route} style={{ textDecoration: 'none' }}> {title} </Anchor> )} </li> ) })} </ul> </div> ) }

Add first MDX page

After creating the theme, you can simply add a MDX file as app/page.mdx and see the result:

Custom theme


Inside your theme layout, you can use CSS imports or other ways to style it. Next.js hooks such as usePathname are also available.

Last updated on