-
Notifications
You must be signed in to change notification settings - Fork 45
theme configuration
Every theme option lives under opts in your jsdoc.json (or, for TypeDoc,
under the cleanJsdocTheme block in typedoc.json — see JSDoc vs
TypeDoc just below). Setting up the rest of the build is
covered in JSDoc Getting Started and TypeDoc Getting
Started; this page documents the theme's own options.
Each option below shows the snippet for both tools in tabs — pick the one that matches your setup.
Warning
Unknown or misspelled options only warn by default (with a "did you mean?"
hint) — the build continues. Set strict to turn those warnings
into errors.
Tip
Don't see an option documented here? Make sure you're on the latest version of the theme — new options ship regularly, and an option that seems missing in your build usually means the installed version predates it.
Every option on this page is the same for both tools — only where you put it
differs. In JSDoc the theme options go under opts; in TypeDoc, under
cleanJsdocTheme.
Theme options live under opts, alongside JSDoc's own options:
{
source: { include: ["./src", "./README.md"] },
plugins: ["plugins/markdown"],
opts: {
destination: "dist",
recurse: true,
template: "node_modules/clean-jsdoc-theme/dist",
// ↓ theme options
siteName: "My Library",
sectionOrder: ["Getting Started", "Classes", "Modules"],
clubSidebarItems: true,
copyPage: { enabled: true, actions: ["copy", "view", "claude"] },
},
}The theme is loaded as a plugin and selected as an output; its options live
under cleanJsdocTheme:
{
entryPoints: ["src/index.ts"],
plugin: ["@clean-jsdoc-theme/typedoc"],
outputs: [{ name: "clean-jsdoc-theme", path: "dist" }],
// ↓ theme options
cleanJsdocTheme: {
siteName: "My Library",
sectionOrder: ["Getting Started", "Classes", "Modules"],
clubSidebarItems: true,
copyPage: { enabled: true, actions: ["copy", "view", "claude"] },
},
}The option names and values are identical — only the namespace changes:
opts (JSDoc) vs cleanJsdocTheme (TypeDoc). One exception: the
outputSourceFiles and
sourceLinkToComment options sit under JSDoc's
templates.default and are JSDoc-only.
The title shown in the header — plain text, or a logo image.
Expected: a string, or a logo set object. The logo set keys are all
strings: light and dark are the logo URLs (or local paths) used in each
theme, default is the fallback when a theme-specific one isn't given, and
alt is the text shown if the image fails to load (and read by screen readers).
opts: { siteName: "My Library" }
// or a logo that swaps with the theme:
opts: {
siteName: {
light: "./assets/logo.svg",
dark: "./assets/logo-dark.svg",
alt: "My Library",
},
}cleanJsdocTheme: { siteName: "My Library" }
// or a logo that swaps with the theme:
cleanJsdocTheme: {
siteName: {
light: "./assets/logo.svg",
dark: "./assets/logo-dark.svg",
alt: "My Library",
},
}Defaults to your package's name.
The site root path the renderer prefixes onto every internal link and asset — set it when the site is served under a sub-path rather than the domain root.
Expected: a string path. Defaults to "" (served at the root).
opts: { basePath: "/my-library" } // served at example.com/my-library/cleanJsdocTheme: { basePath: "/my-library" } // served at example.com/my-library/Your site's public base URL. When set, the build emits a sitemap.xml at the
output root — one entry per page (hidden source-viewer pages are excluded) — which
you can submit to search engines or reference from robots.txt. Without it, no
sitemap is generated.
Expected: an absolute http(s) URL. Only its origin is used for each
page's URL; the deploy sub-path comes from basePath, so the two
never double-count — a bare origin (https://example.com) and a full URL whose
path equals your basePath (https://example.com/my-library) both produce the
same, correct <loc> entries. Omit it (the default) for no sitemap.
opts: { siteUrl: "https://example.com", basePath: "/my-library" }
// → sitemap.xml with https://example.com/my-library/… URLscleanJsdocTheme: { siteUrl: "https://example.com", basePath: "/my-library" }A path to a favicon image. The bridge copies it to a content-hashed
_assets/ asset and emits a <link rel="icon"> (with a type derived from the
extension — .svg → image/svg+xml) into every page's <head>.
Expected: a string file path (.svg, .png, .ico, …), relative to the
working dir. Omitted → no favicon link.
opts: { favicon: "./assets/logo-small.svg" }cleanJsdocTheme: { favicon: "./assets/logo-small.svg" }Tip
An SVG favicon needs this option — browsers only auto-discover a root
favicon.ico, never an SVG. An SVG icon can also adapt to light/dark with a
@media (prefers-color-scheme: dark) block inside the SVG.
A Markdown file rendered as the site home page.
Expected: a path string. (A root docs/index.md overrides it — see
docs.)
opts: { readme: "./README.md" }cleanJsdocTheme: { readme: "./README.md" }A directory of hand-written Markdown/HTML guides rendered as prose pages. The
folder layout drives each page's URL and sidebar group; per-file YAML
frontmatter (title, group, order, slug, hidden) overrides the
defaults, and a root index.md becomes the home page.
Expected: a path string (a directory).
opts: { docs: "./docs" }cleanJsdocTheme: { docs: "./docs" }The display order of the top-level doc groups in the sidebar.
Expected: an array of group-label strings.
opts: { docGroups: ["Getting Started", "Guides"] }cleanJsdocTheme: { docGroups: ["Getting Started", "Guides"] }Tip
This very site is built with the docs and docGroups options — its guides
are plain Markdown files grouped into the sidebar sections you're browsing
right now. Want to build something similar? Browse the source:
docs-site on GitHub.
The group label a doc lands in when it declares none (no frontmatter group
and no folder to humanize).
Expected: a single string.
opts: { defaultDocGroup: "Docs" }cleanJsdocTheme: { defaultDocGroup: "Docs" }The JSDoc --tutorials directory. Each tutorial becomes a guide page, grouped
under "Tutorials" in the sidebar.
Expected: a path string (a directory). Equivalent to JSDoc's -u flag.
opts: { tutorials: "./tutorials" }cleanJsdocTheme: { tutorials: "./tutorials" }The order of all top-level sidebar sections — both your doc/category groups and the API kind labels (Classes, Modules, …). Listed labels come first in the given order; anything you omit is appended afterward.
Expected: an array of section-label strings.
opts: { sectionOrder: ["Getting Started", "Guides", "Classes", "Modules"] }// Accepted but has NO effect under TypeDoc — see the note below.
cleanJsdocTheme: { sectionOrder: ["Getting Started", "Guides", "Classes", "Modules"] }Note
Under JSDoc, sectionOrder orders doc/tutorial groups and the API kind
sections. Under TypeDoc it has no effect at all — that sidebar is a
module/folder hierarchy with its own fixed ordering, and doc groups there are
ordered by docGroups, not sectionOrder. See
TypeDoc flavor.
Collapse related entries (e.g. a module and its members) under a shared,
collapsible parent, grouped by the path segment before the first /.
Expected: a boolean. Defaults to false.
Note
clubSidebarItems has no effect on the TypeDoc API tree. See
TypeDoc flavor.
opts: { clubSidebarItems: true }cleanJsdocTheme: { clubSidebarItems: true }Make top-level sidebar sections collapsible — each section header becomes a
toggle that expands/collapses its entries. Sections default open; a
visitor's collapsed/expanded choice persists per-visitor in localStorage.
Expected: true (or omitted) → every top-level section is collapsible
(default); false → none are; a string[] → only the listed section
labels, matched exactly and case-sensitively ('Class' does not match
'Classes'). The labels are the rendered section headers: the plural kind
labels (Classes, Namespaces, Interfaces, Modules, Typedefs/Type Aliases, Enumerations, Functions, Variables, Globals), @category
top-level segments, doc-group labels, Tutorials, and Source Files. An array
entry that matches no rendered section prints a build warning listing the
sections that are available.
Note
No function form — the value is resolved once, at build time. Unlike
sectionOrder / clubSidebarItems, collapsibleSidebarSections works
identically under both JSDoc and TypeDoc.
opts: {
// all top-level sections collapsible (default if omitted)
collapsibleSidebarSections: true,
// …or only these (exact, case-sensitive labels):
collapsibleSidebarSections: ["Namespaces", "Classes"],
}cleanJsdocTheme: {
collapsibleSidebarSections: true,
// …or only these (exact, case-sensitive labels):
collapsibleSidebarSections: ["Namespaces", "Classes"],
}Custom links pinned above the sidebar navigation.
Expected: an array of entries. Each is an object with title, a link (or
href), an optional id, and an optional icon — lucide:<name> or
simpleicons:<name>, loaded from a CDN. Two optional fields control link
presentation: target (the link target — an external link defaults to _blank)
and class (extra CSS class(es) merged onto the rendered link).
opts: {
menu: [
{ title: "Home", link: "/", icon: "lucide:home" },
{ title: "GitHub", link: "https://github.com/you/repo", icon: "simpleicons:github" },
// Open in the same tab + tag with a custom class for styling via customCss:
{ title: "Changelog", link: "/changelog", target: "_self", class: "menu-changelog" },
],
}cleanJsdocTheme: {
menu: [
{ title: "Home", link: "/", icon: "lucide:home" },
{ title: "GitHub", link: "https://github.com/you/repo", icon: "simpleicons:github" },
{ title: "Changelog", link: "/changelog", target: "_self", class: "menu-changelog" },
],
}The prev/next pager at the foot of each content page — two cards linking the adjacent pages in sidebar reading order. On by default; source-viewer pages never show it.
Expected: a boolean shorthand, or an object { enabled }. Defaults to true.
opts: { pageNav: false }
// or the object form: opts: { pageNav: { enabled: false } }cleanJsdocTheme: { pageNav: false }
// or the object form: cleanJsdocTheme: { pageNav: { enabled: false } }Override the type families.
Expected: an object with heading, body, and/or mono. heading and
body are Google Font family names (loaded for you, existence-checked at build
time); mono is a CSS font stack.
opts: {
fonts: { heading: "Fraunces", body: "Spline Sans", mono: "Spline Sans Mono" },
}cleanJsdocTheme: {
fonts: { heading: "Fraunces", body: "Spline Sans", mono: "Spline Sans Mono" },
}Recolor the theme. colors is the light-mode palette (also the :root
default); darkColors is the dark-mode palette, emitted under
[data-theme="dark"]. Each merges per key over the built-in palette, so you
can override just bg and keep every other default.
Expected: an object with any subset of these keys, each a CSS color string (the theme ships oklch, but any valid CSS color works):
| Key | Role |
|---|---|
bg |
Page background |
bgMuted |
Subtle surfaces (code blocks, cards, sidebar) |
fg |
Body text |
fgMuted |
Secondary / muted text |
accent |
Links, focus rings, primary buttons |
accentFg |
Text/icon on an accent background |
border |
Hairlines and dividers |
codeHeaderBg |
Code-block header strip background |
codeHeaderFg |
Code-block header label text |
codeHighlightBg |
Highlighted code-line background (@playground / highlight=) |
The three code* keys style the code-block chrome — the header strip (its
background + the CODE/filename label) and the tint on a highlighted line. They
default to a neutral #f7f7f7-equivalent in light and elevated greys in dark, so
you only set them to match a custom palette.
opts: {
colors: { bg: "oklch(0.99 0.01 95)", accent: "oklch(0.55 0.2 250)" },
darkColors: { bg: "oklch(0.18 0.01 250)", accent: "oklch(0.7 0.16 250)" },
}cleanJsdocTheme: {
colors: { bg: "oklch(0.99 0.01 95)", accent: "oklch(0.55 0.2 250)" },
darkColors: { bg: "oklch(0.18 0.01 250)", accent: "oklch(0.7 0.16 250)" },
}Unknown keys and non-string values are ignored. If you omit darkColors
entirely, dark mode falls back to a sensible bg/fg swap of colors.
Controls how the scrollbar (page and any scrollable panel, e.g. the sidebar or a code block) is presented.
Expected: one of "styled" (default), "visible", or "native".
-
"styled"— the theme's thin, colored overlay bar, invisible at rest and only painted while actively scrolling or on hover. -
"visible"— the same thin, themed bar, but always shown (no idle-hide). -
"native"— no scrollbar styling at all; the browser's own, always-visible scrollbar.
The default overlay hides at rest, which can make your position hard to judge
in a long page (#281).
Set scrollbar to "visible" or "native" if you'd rather the scrollbar stay
put. This option applies to both JSDoc and TypeDoc.
opts: {
// "styled" (default) | "visible" | "native"
scrollbar: "native",
}cleanJsdocTheme: {
// "styled" (default) | "visible" | "native"
scrollbar: "native",
}Inline CSS/JS injected into every page. Custom CSS loads after the theme stylesheet (so it overrides); custom JS runs last.
Expected: a string.
opts: { customCss: ".my-banner { color: rebeccapurple; }" }cleanJsdocTheme: { customCss: ".my-banner { color: rebeccapurple; }" }Like the above, but read from a file on disk. The bridge copies each to a content-hashed asset and links it.
Expected: a path string.
opts: { customCssFile: "./extra.css", customJsFile: "./extra.js" }cleanJsdocTheme: { customCssFile: "./extra.css", customJsFile: "./extra.js" }Whether custom-asset filenames are content-hashed (for cache-busting). Set
false to keep stable, unhashed names.
Expected: a boolean. Defaults to true.
opts: { hashCustomAssets: false }cleanJsdocTheme: { hashCustomAssets: false }A custom site footer, rendered in place of the default footer on every page.
Either an inline HTML string or { file: "./footer.html" } — the bridge reads
the file at build time, so render() stays pure. It's trusted, author-controlled
HTML (rendered verbatim, like v4's footer); style it with
customCss / customCssFile, which load after the
theme stylesheet so your selectors win.
Expected: a string, or { file }.
opts: { footer: "<div class='site-footer'>© 2026 My Library</div>" }
// or, for anything longer than a line:
opts: { footer: { file: "./footer.html" } }cleanJsdocTheme: { footer: "<div class='site-footer'>© 2026 My Library</div>" }
// or:
cleanJsdocTheme: { footer: { file: "./footer.html" } }Site-wide custom <meta> tags. Pass an array of attribute objects — each
object's key/value pairs become the attributes of one <meta> tag in <head>
(name/content, property/content, http-equiv, charset, …). The theme
emits charset, viewport, a generator tag (clean-jsdoc-theme <version>,
so a built site records which version produced it), and an auto description
itself and de-dupes: an author entry sharing an identifying attribute
(name / property / http-equiv / charset) replaces the theme's default
rather than duplicating it (so your description — or a custom generator —
wins). Values are escaped automatically; invalid
attribute names are dropped. These are site-wide (no per-page meta yet).
Expected: an array of { [attr]: value } objects.
opts: {
meta: [
{ name: "description", content: "Fast, typed docs for My Library" },
{ name: "keywords", content: "jsdoc, typescript, docs" },
{ property: "og:title", content: "My Library" },
{ property: "og:image", content: "https://example.com/card.png" },
{ name: "theme-color", content: "#0b0b0b" }
]
}cleanJsdocTheme: {
meta: [
{ name: "description", content: "Fast, typed docs for My Library" },
{ property: "og:title", content: "My Library" }
]
}The per-page "copy page" / "open in LLM" button (content pages only).
Expected: a boolean shorthand, or an object { enabled, actions } where
actions is any of copy, view, claude, chatgpt, perplexity. On by
default with all actions.
opts: { copyPage: { enabled: true, actions: ["copy", "view", "claude"] } }
// or simply: opts: { copyPage: false }cleanJsdocTheme: { copyPage: { enabled: true, actions: ["copy", "view", "claude"] } }
// or simply: cleanJsdocTheme: { copyPage: false }A custom instruction prepended when a page is handed to an LLM via the open-in actions.
Expected: a string.
opts: { aiPrompt: "You are helping a developer use My Library. " }cleanJsdocTheme: { aiPrompt: "You are helping a developer use My Library. " }[!INFO] These two are JSDoc-only. They live under
templates.default(JSDoc's default-template namespace), not underoptsorcleanJsdocTheme.
Whether to generate the syntax-highlighted source-file viewer pages and the
Source: file:line links on members.
Expected: a boolean. Defaults to true; set false to suppress both.
templates: { default: { outputSourceFiles: false } }Where a Source: link lands: the symbol's declaration (default) or its
documentation comment.
Expected: a boolean. Defaults to false (land on the declaration).
templates: { default: { sourceLinkToComment: true } }You don't configure this, but it's worth knowing how local files referenced from
your docs, tutorials, README, and API doc comments are processed. Any image
you link with a relative or root-relative path —  —
is copied into the site's _assets/ directory under a content-hashed name
(e.g. _assets/flow.3de65053.svg) and the reference is rewritten to point at it.
The hash is derived from the file's bytes, so an unchanged file keeps a stable,
cacheable URL across builds and a changed one cache-busts automatically. External
(https://…) and data: URLs are left untouched. (See
Working with images for the full guide.)
.svg files get one extra step: their markup is inlined directly into the
page rather than loaded through an <img>. That lets an SVG's own
[data-theme="dark"] styles follow the in-page theme toggle — an <img>-loaded
SVG can only see the operating system's color scheme, never your site's toggle.
Logos (siteName) and
customCssFile / customJsFile ride the same
content-hashed _assets/ pipeline.
If you use JSDoc's standard templates.default.staticFiles.include to ship a
folder of images (the convention where files land at the output root and you
reference them by bare name, e.g. ), the theme
honors it: those directories become fallback search roots, so a bare or
root-relative reference resolves and flows through the same content-hashed
_assets/ pipeline above — no need to rewrite your comments to relative paths.
Any non-image files in those directories are still copied verbatim to the
site root, matching JSDoc's behavior. (A file the image pipeline already served
from _assets/ isn't copied to the root a second time.)
These two opt your site into a multi-language build — one static site per
locale, with a header language switcher and hreflang alternates wired in. They
declare the locales; the actual translation workflow (extracting catalogs,
translating, and building each locale) runs through the
clean-jsdoc CLI. The full walkthrough is in
Localize your docs. A build with no locales is
unaffected — it renders exactly as before.
[!INFO] Localized builds are JSDoc-only today. The TypeDoc bridge can extract catalogs but does not yet render the per-locale sites.
The languages to build.
Expected: an array of { code, name } objects (the name is the switcher
label), or bare locale-code strings. Codes are BCP-47-ish (en, pt-BR).
opts: {
locales: [
{ code: "en", name: "English" },
{ code: "ja", name: "日本語" },
],
}cleanJsdocTheme: {
locales: [
{ code: "en", name: "English" },
{ code: "ja", name: "日本語" },
],
}The language rendered at the site root (every other locale lands under
/<code>).
Expected: a locale code that appears in locales. Optional —
defaults to the first entry.
opts: { defaultLocale: "en" }cleanJsdocTheme: { defaultLocale: "en" }Escalate option diagnostics (a bad font name, an unknown key) from warnings to hard build errors.
Expected: a boolean. Defaults to false.
opts: { strict: true }cleanJsdocTheme: { strict: true }Toggle the build's progress output (the per-stage spinners).
Expected: a boolean. Defaults to true.
opts: { progress: false }cleanJsdocTheme: { progress: false }This wiki is auto-generated from docs-site/docs. Edit there — changes sync automatically. Full docs: https://ankdev.me/clean-jsdoc-theme/