Theming
Restyle PzzaDocs from one config object, from global CSS tokens, or from the stable class names.
PzzaDocs never hardcodes a colour, size or font. Every value in the stylesheet comes from a --pd-* custom property, and the whole look is a thin layer of those tokens on top of a monochrome base. You can change that layer at three levels, from the least to the most involved:
- The
themesection ofdocs.config.ts. Pick a preset, override colours, set the radius, fonts, layout widths, density and the code theme. Everything stays in one typed object and is emitted as CSS at render time. - Token overrides in
app/globals.css. Redefine any--pd-*property on:rootand.dark. Useful when the values already live in your own design system. - Class-based CSS. Every element the framework renders carries a stable
pd-*class and state attributes, so you can restyle a single piece of chrome without touching the rest.
Most sites only need the first level. The other two are covered in CSS overrides.
Quick example#
import { defineConfig } from "pzzadocs";
export default defineConfig({
title: "Acme Docs",
theme: {
preset: "warm",
radius: 6,
density: "compact",
fonts: {
sans: "var(--font-geist-sans), system-ui, sans-serif",
mono: "var(--font-geist-mono), ui-monospace, monospace",
},
},
});This switches the palette to the warm paper-and-ink preset, rounds every control to 6px, tightens the row heights and paddings, and points the two font tokens at variables published by next/font. Nothing else changes.
How it is applied#
RootProvider renders a server component called ThemeStyles. It reads the resolved theme config and emits a single <style> element containing a :root { ... } block for the light values and a .dark { ... } block for the dark values. The CSS is computed on the server during render, so:
- there is no runtime JavaScript involved in applying the theme,
- the first paint already uses your tokens, with no flash of the default look,
- only the keys you actually set are emitted; an empty
themeproduces no<style>at all.
A preset expands to its full colour set first, then your explicit colors are layered on top. Numbers become pixel values (radius: 6 becomes 6px); strings are used as written. Setting radius also derives --pd-radius-sm as max(2px, calc(radius - 1px)) for inline code and keyboard keys.
If you build your own provider instead of using RootProvider, import ThemeStyles from pzzadocs/ui and render it once near the top of your tree with the resolved config: <ThemeStyles theme={config.theme} />. The signature and the underlying resolveThemeCss helper are documented in the Theme API reference.
Fonts#
Fonts are not bundled. Load them in your root layout with next/font and point the fonts tokens at the CSS variables it publishes:
import { Geist, Geist_Mono } from "next/font/google";
const sans = Geist({ subsets: ["latin"], variable: "--font-geist-sans" });
const mono = Geist_Mono({ subsets: ["latin"], variable: "--font-geist-mono" });
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={`${sans.variable} ${mono.variable}`} suppressHydrationWarning>
<body>{children}</body>
</html>
);
}theme: {
fonts: {
sans: "var(--font-geist-sans), system-ui, sans-serif",
mono: "var(--font-geist-mono), ui-monospace, monospace",
},
}fonts.sans sets --pd-font-sans (body and UI text) and fonts.mono sets --pd-font-mono (code). Both accept any font-family value. Keep the variable classes on <html> so portals such as the search dialog and the image zoom inherit them too.
Radius, density and layout#
theme: {
radius: 8,
density: "compact",
layout: {
sidebarWidth: 260,
tocWidth: 200,
contentWidth: "80ch",
headerHeight: 56,
},
}radiussets--pd-radiusfor cards, code blocks, buttons, tabs and dialogs. Defaults to4.densityswitches between"comfortable"(the default) and"compact". Compact lowers the sidebar and TOC row height from 28px to 24px and reduces block gaps, table cell padding, box padding and card padding. The exact values are listed in the density tokens.layoutmaps directly onto--pd-sidebar-width(240px),--pd-toc-width(220px),--pd-content-width(72ch) and--pd-header-height(48px). Numbers are pixels, strings are used verbatim, so"80ch"and"min(72ch, 90vw)"both work.
Code themes#
Code is highlighted with a pair of bundled themes, pzza-light and pzza-dark. They are deliberately subdued: the UI around the code is gray, so the token colours are the only hues on the page and stay low in saturation.
Adjusting the bundled themes#
The code section overrides individual token groups of the bundled pair. Set only what you want to change:
theme: {
code: {
light: { keyword: "#4338ca", comment: "#8a8a8a" },
dark: { keyword: "#a5b4fc", string: "#86efac" },
},
}The available groups are plain, keyword, fn, string, number, type, tag, attribute, property, variable, comment, punctuation, regex, templatePunctuation and background. Their defaults are listed in the CodeTokenColors reference.
Using a different theme pair#
codeTheme replaces the bundled pair with any two shiki theme registrations. Import them from shiki/themes or pass your own objects:
import light from "shiki/themes/<name>.mjs";
import dark from "shiki/themes/<name>.mjs";
export default defineConfig({
title: "Acme Docs",
theme: {
codeTheme: { light, dark },
},
});code is ignored with a custom pair
When codeTheme is a custom pair the code overrides are not applied, because they describe token groups of the bundled themes. Set codeTheme: "pzza" (the default) to use the bundled pair together with code.
The code block chrome still uses the regular tokens: --pd-surface-2 for the header, --pd-surface-3 for highlighted lines and --pd-border-strong for word highlights. The body background follows the theme's own background.
Next steps#
Presets
Nine built-in palettes with every value, a live picker, and how to extend one.
CSS overrides
Cascade layers, token overrides in globals.css, and the stable class and state attribute reference.
Dark mode
How the theme switches, the D hotkey, forcing a theme, and native control colours.
Theme API
Every field of ThemeConfig, the token tables, ThemeStyles and resolveThemeCss.