Adding a Preset
This page is for contributors extending the codebase with a new preset. End users selecting from the existing presets do not need this page — see the Presets section instead.
Why a sample preset exists
The codebase ships a deliberately-empty preset called sample that lives at:
src/presets/sample/preset.tssrc/types/presets/sample/preset.d.tssrc/styles/presets/sample/preset.csssrc/styles/presets/sample/blocks/**src/styles/presets/sample/theme/**assets/presets/sample/logo.svg
It is not a user-selectable preset. It is intentionally excluded from Shared_PresetName (the type union that gates which preset names can be passed to preset: in docusaurus.config.ts), from the registry exports in src/presets/index.ts, and from src/lib/shiki-themes.ts. A consumer who tries to write preset: 'sample' gets a TypeScript error and a runtime validation error.
The reason it exists in the source tree at all is to serve as a copy-pastable scaffold. Without it, anyone adding a new preset has to pattern-match against whichever existing preset happens to be most fleshed-out, which causes new presets to inherit unintended visual characteristics from their reference. Copying from sample/ instead gives you the full surface area to fill in (every Nova block, every Docusaurus theme component) without inheriting any visual identity.
The scaffold mirrors the full surface of the shared structural CSS — all 10 Nova block directories and all 105 Docusaurus theme component directories that have a style.css of their own. Each stub is empty by design; you decide whether to leave it empty (the surface inherits only the shared structural base) or fill it with preset-specific identity.
The nine schema slots
A preset is configured through one object that matches the Shared_Preset type. Every preset declares values for these nine slots:
| Slot | Type | Purpose |
|---|---|---|
logo | { title, alt, src } | Brand mark shown in the navbar. |
colors | { primary, accent, text, border, warning, danger } (each a { light, dark } pair of hex strings) | Six color categories. primary and accent are expanded to 11-stop scales (50 through 950); text, border, warning, and danger emit a single base token per category plus derived color-mix variants. Each category has a light and dark hex. |
fonts | { display, body, code } | Three Google Font families that the runtime auto-injects via <link> tag at build time. |
shape | { radius, density } | Toggles which shared shape variants apply (rounded vs sharp, comfortable vs compact). |
depth | { cards, codeBlocks } | Toggles which shared depth variants apply (flat vs elevated cards, etc.). |
motion | { speed, staggeredReveals, hoverEffects } | Controls animation cadence and whether decorative motion is enabled. |
navbar | 'bridge' | 'canopy' | 'compass' | 'monolith' | Which navbar layout variant to activate. |
footer | 'commons' | 'embassy' | 'ledger' | 'launchpad' | Which footer layout variant to activate. |
cta | { contained } | Whether the footer CTA banner is width-constrained via nova-container (true) or runs full-bleed (false). |
Note: the preset's internal colors.accent key is exposed to consumers under the name colors.secondary in overrides. Inside the preset source you use accent; consumers writing docusaurus.config.ts use secondary. The consumer-facing override API only covers colors, fonts, navbar, and footer — your preset's shape, depth, motion, and cta settings are part of its identity and cannot be overridden by consumers.
Cookbook
1. Copy the scaffold
Replace <name> with your preset's chosen name throughout. The name must be lowercase and must not collide with the existing preset names.
PKG=packages/docusaurus-preset-nova
cp -R $PKG/src/presets/sample $PKG/src/presets/<name>
cp -R $PKG/src/types/presets/sample $PKG/src/types/presets/<name>
cp -R $PKG/src/styles/presets/sample $PKG/src/styles/presets/<name>
cp -R $PKG/assets/presets/sample $PKG/assets/presets/<name>
2. Rename the constant and type
In src/presets/<name>/preset.ts:
import type { Presets_Foo_Preset_Foo } from '../../types/presets/<name>/preset.d.ts';
export const presetsFooPresetFoo: Presets_Foo_Preset_Foo = {
// ...
};
Replace Foo with the preset name in PascalCase (e.g., Lantern → type Presets_Lantern_Preset_Lantern, const presetsLanternPresetLantern). Per the 0.18.0 convention, types use underscore-separated segments and const variables stay camelCase.
In src/types/presets/<name>/preset.d.ts:
import type { Shared_Preset } from '../../shared.d.ts';
export type Presets_Foo_Preset_Foo = Shared_Preset;
3. Fill in the nine slots
Edit src/presets/<name>/preset.ts and replace every value with your preset's identity:
export const presetsFooPresetFoo: Presets_Foo_Preset_Foo = {
logo: {
title: 'Foo',
alt: 'Foo',
src: '@nova-assets/presets/<name>/logo.svg',
},
colors: {
primary: { light: '#000000', dark: '#000000' }, // brand color
accent: { light: '#000000', dark: '#000000' }, // secondary accent
text: { light: '#000000', dark: '#000000' }, // body text base
border: { light: '#000000', dark: '#000000' }, // dividers and outlines
warning: { light: '#000000', dark: '#000000' }, // warning admonition base
danger: { light: '#000000', dark: '#000000' }, // danger/error admonition base
},
fonts: {
display: 'Font Name', // headings (Google Fonts only)
body: 'Font Name', // body text
code: 'Font Name', // monospaced code
},
shape: {
radius: 'sharp' | 'rounded' | 'pill',
density: 'compact' | 'comfortable' | 'spacious',
},
depth: {
cards: 'flat' | 'elevated' | 'glass',
codeBlocks: 'flat' | 'bordered' | 'elevated',
},
motion: {
speed: 'none' | 'subtle' | 'normal' | 'expressive',
staggeredReveals: true | false,
hoverEffects: true | false,
},
navbar: 'bridge' | 'canopy' | 'compass' | 'monolith',
footer: 'commons' | 'embassy' | 'ledger' | 'launchpad',
cta: {
contained: true | false, // footer CTA width-constrained (nova-container) vs full-bleed
},
};
Replace the placeholder logo SVG at assets/presets/<name>/logo.svg with a real mark.
4. Register the preset
Add to Shared_PresetName
In src/types/shared.d.ts, add <name> to the Shared_PresetName union, alphabetically:
export type Shared_PresetName = 'envoy' | 'foundry' | '<name>' | /* ... */;
Register in the index
In src/presets/index.ts, import the preset and add it to both presetsIndexNames (alphabetical) and presetsIndexPresets:
import { presetsFooPresetFoo } from './<name>/preset.js';
export const presetsIndexNames: Presets_Index_Names = [
// ...alphabetical
'<name>',
// ...
];
export const presetsIndexPresets: Presets_Index_Presets = {
// ...alphabetical
<name>: presetsFooPresetFoo,
// ...
};
Add a Shiki theme pair
In src/lib/shiki-themes.ts, map your preset name to a Shiki theme pair (light + dark) for code-block syntax highlighting. Use any pair from the Shiki theme list.
5. Layer your identity
This is the bulk of the work. The scaffold has a style.css stub for every Nova block and every Docusaurus theme component that has shared structural styling. You can:
- Leave a stub empty — the surface inherits only the shared structural base.
- Fill the stub — your CSS layers on top of the shared base, scoped by preset selection at runtime.
For an identity to feel cohesive, fill as many surfaces as the design demands. Conservative presets reach maybe a third of the surface. Committed presets reach most of it.
The CSS load order at runtime:
shared global vars
→ generated --nova-color-{primary,accent}-{50..950} scales
→ generated --nova-color-{text,border,warning,danger} base tokens + derived color-mix tokens
→ shared block CSS (src/styles/blocks/**)
→ shared theme CSS (src/styles/theme/**)
→ preset's preset.css (your tokens, accessibility floor, scrollbar, etc.)
→ preset's blocks/** (your Nova block overlays)
→ preset's theme/** (your Docusaurus theme component overlays)
Conventions:
- No
@importstatements inpreset.css. The runtime auto-loads your three Google Fonts via<link>tag injection. Adding@importbypasses the consumer'soverrides.fonts.*mechanics. - Reference brand color shades via
var(--nova-color-{primary,accent}-{50..950}), body text and surfaces viavar(--nova-color-{text,text-muted,text-soft,text-inverse,border,border-subtle,surface-raised}), and admonition colors viavar(--nova-color-{warning,danger}-{500,400,bg})— or compose viacolor-mix(in srgb, ...). Avoid raw hex insidetheme/overlays; declare hex only inpreset.cssfor surface tokens. - Reference fonts via
var(--nova-font-{display,body,code}). Never hardcode font names in selectors. - Provide light + dark mode parity for every visible color (
:rootdefaults,[data-theme="dark"]overrides). - Keep
:focus-visibleoutlines and theprefers-reduced-motionblock fromsample/preset.css. They are the accessibility floor.
6. Add tests
Add expectations for your preset to:
src/tests/presets/index.test.ts(registry membership and schema)src/tests/lib/shiki-themes.test.ts(Shiki theme pair lookup)src/types/tests/presets/index.test.d.ts(type aliases for the test assertions)
Run npm run check:test to confirm.
7. Document the preset
Create a user-facing docs page at:
apps/docs/docs/facades/docusaurus-preset/presets/<name>.mdx
Mirror the structure of the existing preset pages (Why Use → Preview → Summary → Configuration → Overrides → Navbar and Footer). Set sidebar_position so presets stay alphabetical.
Add light/dark preview screenshots at:
apps/docs/docs/facades/docusaurus-preset/presets/_assets_/<name>-light.png
apps/docs/docs/facades/docusaurus-preset/presets/_assets_/<name>-dark.png
8. Add a demo app (optional but recommended)
Clone an existing demo app and adapt it:
cp -R apps/demo-foundry apps/demo-<name>
Update package.json, docusaurus.config.ts, content directories, and static assets to match the new preset's identity. The demo app is what gets captured for the preview screenshots in the docs page.
9. Verify
cd packages/docusaurus-preset-nova && npm run check
cd apps/demo-<name> && npm run dev
npm run check runs lint, typecheck, and the test suite. npm run dev boots the demo so you can verify the preset visually.
What sample does NOT contain
For the avoidance of doubt, the sample scaffold is intentionally absent from:
Shared_PresetName(type union insrc/types/shared.d.ts) — so consumers cannot typepreset: 'sample'.presetsIndexNamesandpresetsIndexPresets(insrc/presets/index.ts) — so the runtime registry never sees it.src/lib/shiki-themes.ts— so no Shiki theme is mapped.apps/docs/docs/facades/docusaurus-preset/presets/— there is no consumer-facing preset doc page.apps/— there is nodemo-sampleapp.- The Joi schema allow-list — runtime validation rejects the name.
These exclusions are the mechanism that makes sample invisible to consumers while keeping it visible to contributors as a copy-pastable template.