Themes

On this page

Themes

Themes are native BoxLang .bxm templates - there's no separate template engine or build step involved.

Built in

ThemeBaseNotes
bootstrap (default)Bootstrap 5, vendoredPoppins font, brand gradient navbar
materialHand-rolled Material-style CSSCard layout, elevation shadows, Roboto font
tailwindTailwind Play CDNUtility-class driven, no build step
docsyHand-rolled CSS, forked from materialRead the Docs/Docsy-inspired navy-blue reference-manual look
slateHand-rolled CSS, forked from materialStripe/Slate-inspired - a permanently dark sidebar regardless of light/dark mode
docusaurusHand-rolled CSS, forked from materialDocusaurus-inspired bold full-width colored navbar, rounded cards
justthedocsHand-rolled CSS, forked from materialJust the Docs-inspired minimalism - search box lives at the top of the sidebar
vuepressHand-rolled CSS, forked from materialVuePress-inspired green accent, soft rounded corners
gitbookHand-rolled CSS, forked from materialGitBook-inspired centered reading column, serif headings
notionHand-rolled CSS, forked from materialNotion-inspired borderless sidebar, near-grayscale UI, generous whitespace

The seven material-forked themes above reuse material's exact BoxLang templates (layout.bxm/page.bxm/search.bxm) unchanged except for a scoped CSS-class-prefix rename - only assets/style.css (and, for justthedocs, one relocated <bx:include> line moving the search box into the sidebar) differs, so they inherit the same full feature set and the same air-gapped-capable behavior material already has.

Every built-in theme's own CSS/JS (Bootstrap's CSS/JS bundle, highlight.js, Alpine.js, MiniSearch for the default local search provider, and Mermaid when mermaid is turned on) ships vendored with this module and is copied straight into every built site/ - no CDN, no internet access needed to view a built site. The tailwind theme's own utility engine (a client-side JIT compiler, not a static stylesheet) and other optional features you turn on yourself (math, Algolia search, Google Analytics) still load from a CDN or a hosted API - see Air-gapped/offline sites below.

bootstrap, material and tailwind apply the same BoxLang brand palette (a #00FF78 -> #00DBFF gradient and a #FFF500 accent); the seven gallery themes below them each use their own distinct palette instead, inspired by whichever platform they borrow their look from - see the table above. Every one of the ten ships the same set of page features regardless of palette:

  • An in-page "On this page" table of contents, generated from each page's own h2/h3 headings.
  • Breadcrumbs, showing a page's ancestor chain when it's nested more than one level deep under a linked ancestor.
  • Prev/next page links at the bottom of the article, following the nav's own reading order.
  • Syntax-highlighted code blocks, via highlight.js plus a BoxLang grammar of its own (```bx/```boxlang/```cfscript), each with a copy button - shown on hover on devices that support it, always visible on touch devices (there's no hover to reveal it there). See Markdown Extensions.
  • Self-hosted webfonts - no fonts.googleapis.com request at view time.
  • A dark/light mode toggle, powered by Alpine.js for reactivity. The visitor's choice is remembered in localStorage (falling back to their OS preference), and applied before first paint to avoid a flash of the wrong theme.
  • A responsive header that stays a single row at every width - a narrow viewport shrinks the search box rather than wrapping it onto its own line - plus a collapsible sidebar nav (a hamburger toggle in bootstrap/material/tailwind alike).
  • Keyboard shortcuts in the search box: / focuses search from anywhere on the page, and Escape closes the results. See Search.
  • A repo link and "Edit this page"/"Last updated" line, when bxsites.yaml's repo/lastUpdated options are set. See Configuration.
  • A "Download Markdown" link, next to "Edit this page" - every page's raw .md source is published alongside its built HTML (guides/themes.md sitting next to guides/themes/index.html), so it (or an LLM) can read the page as plain Markdown directly instead of parsing rendered HTML. Always on, no config needed. See Getting Started.
  • An opt-in footer (copyright, social links, a "Built with BxSites" credit) when bxsites.yaml's footer is true. See Configuration.
  • A version switcher, appearing automatically once a project has a docs/versions/ folder with more than one version in it. See Configuration.
  • A themed 404.html, served automatically by most static hosts (including GitHub Pages) for any unmatched path. Add a 404.md at the root of docs/ (or src/) to override its title and body with your own - it's never built as a regular page (no nav entry, no sitemap.xml URL), just rendered into site/404.html in its place.
  • A custom logo and favicon, when bxsites.yaml's theme.logo/ theme.favicon are set. See Configuration.
  • A collapsible sidebar nav, opt-in via theme.options.navCollapsible - every section with children (linked or not) gets a toggle button instead of always showing its children inline, and the section containing the current page always starts open. See Configuration.
  • Google Analytics, when bxsites.yaml's analytics is configured. See Configuration.
  • Social share cards (Open Graph + Twitter Card meta tags), sourced from each page's description frontmatter (or the site-wide description) and its own ogImage (or the site-wide one) - optionally auto-generated per page via bxsites.yaml's generateOgImages. See Configuration.
  • Page tags, an icon and a summary line, all opt-in via a page's own frontmatter - tags render as badges linking into a site-wide /tags/ index. See Getting Started.
  • An explicit nav override, in bxsites.yaml or its own docs/nav.json, replacing folder-inference for large sites. See Configuration.
  • Extra CSS/JS, injected via bxsites.yaml's extraCss/extraJs. See Configuration.
  • Admonition (note/warning/tip/...) callout boxes, on by default in any page's markdown, including collapsible variants - no config needed. See Markdown Extensions.
  • Footnotes and definition lists, opt-in via bxsites.yaml's markdown. See Markdown Extensions.
  • Content tabs, code line numbers/highlighted lines/titles, and diff markers/terminal-style frames for code blocks, no config needed. See Markdown Extensions.
  • Responsive images - resized + WebP variants and a <picture> rewrite for every eligible docs/assets/** image, on by default. See Responsive Images.
  • Mermaid diagrams, opt-in via bxsites.yaml's mermaid. See Markdown Extensions.
  • Math (KaTeX), opt-in via bxsites.yaml's math. See Markdown Extensions.

Set which one a project uses in bxsites.yaml:

theme: { name: material }
{ "theme": { "name": "material" } }

[theme]
name = "material"

Installing a published theme

A theme published to ForgeBox installs with nothing but the bxSites binary itself - no box/CommandBox needed. Browse what's already published under the bxsites-themes category on ForgeBox:

bxSites install:theme --name=bx-sites-theme-blog1 [--version=1.0.0]

This downloads the package's zip and extracts it into .themes/bx-sites-theme-blog1/ inside the project's content root (see Content Source), validating it satisfies the ThemeProvider contract below before finishing. A project can carry several installed themes side by side this way and switch between them purely by name:

theme: { name: bx-sites-theme-blog1 }
{ "theme": { "name": "bx-sites-theme-blog1" } }

[theme]
name = "bx-sites-theme-blog1"

A theme needs no BoxLang module/class-loader involvement at all (unlike a plugin) - it's pure files, so there's no separate activation step the way install:plugin has; setting theme.name is the only wiring needed. See install:theme in the CLI reference.

Starting from a theme built for a different static site generator instead? See Importing a theme - theme:import mechanically converts an mkdocs/jekyll/hugo theme's own template files into a best-effort .themes/<name>/ scaffold.

Air-gapped/offline sites

A built site works with no internet access at all by default, for bootstrap, material, and the seven material-forked themes (docsy, slate, docusaurus, justthedocs, vuepress, gitbook, notion) with the default local search provider: Bootstrap's own CSS/JS, highlight.js, Alpine.js, and MiniSearch are all vendored with this module (resources/assets/vendor/) and copied straight into site/assets/vendor/ at build time - no CDN <script>/<link> tag anywhere in the generated HTML for any of those. Turning on bxsites.yaml's mermaid key vendors Mermaid the same way - its mermaid.min.js bundle is copied into site/assets/vendor/mermaid/ and every built-in theme loads it from there, so diagrams still render with zero outbound requests.

A few things still reach out to the network, only when you turn them on yourself:

  • The tailwind theme's own utility engine is a client-side JIT compiler loaded from cdn.tailwindcss.com - it isn't a static stylesheet this module can vendor the same way, so this theme isn't air-gapped-capable yet.
  • Mermaid's own layout engine lazy-loads one extra chunk, elk-api.js, from jsDelivr - but only for diagram types that opt into the elk layout algorithm; the vendored mermaid.min.js renders every other diagram type entirely on its own.
  • bxsites.yaml's math option loads KaTeX (both its JS and its own font files) from a CDN when turned on.
  • searchProvider.provider: "algolia" and analytics.provider: "google" inherently talk to a hosted API/tracking endpoint - vendoring the JS file wouldn't remove that dependency.

If your deployment target genuinely has zero internet access, stick to bootstrap/material/one of the seven material-forked themes, the default local search provider, avoid elk-layout Mermaid diagrams if mermaid is on, and leave math/Algolia/analytics off.

See Icons for how a page's own icon frontmatter (or a nav.json entry's own icon) resolves to an emoji, a named icon from one of eight bundled libraries, or a project's own custom SVG.

The ThemeProvider contract

A theme is just a folder with:

  • layout.bxm (required) - the outer HTML shell + nav. Receives variables.page, variables.nav, variables.siteConfig, variables.themeDir and variables.basePath in scope, and includes the resolved body template via #variables.themeDir#/#variables.bodyFile# (page.bxm unless a page/content type resolves to something else - see Multiple layouts per page below). variables.basePath is always a root-relative path ending in / (/ by default, /my-docs/ when bxsites.yaml's baseURL overrides it) - prefix every internal href/src with it, rather than hardcoding a leading /, so the theme still works when the site is served from a sub-path.
  • page.bxm (required) - the article body. Renders variables.page.contentHtml - the already-converted markdown.
  • search.bxm (optional) - the search box markup, included by layout.bxm only when bxsites.yaml's search is true. See Search.
  • blog.bxm (optional) - an alternate outer shell for the blog's own listing/category/archive/author/stats pages. Falls back to layout.bxm when a theme doesn't have it. See Multiple layouts per page.
  • blog-page.bxm (optional) - an alternate body for an individual blog post, still rendered under the theme's normal layout.bxm. Falls back to page.bxm when a theme doesn't have it.
  • assets/ (optional) - theme CSS/JS, copied to site/assets/theme/ at build time.

variables.page.editUrl/.lastUpdated (empty strings when unconfigured) and variables.siteConfig.repo/.social/.footer are always available too, backing the repo link/edit link/last-updated/footer features above - a custom theme decides for itself whether and how to render them, same as everything else. variables.versions ([ { label, url, kind } ], "Latest" first) and variables.currentVersion (the label being rendered right now) back the version switcher - empty/"Latest" for a project that isn't versioned, so a theme only needs to render a switcher when variables.versions.len() gt 1. Each entry's kind is "current" (the version a visitor lands on at the site root), "released" (any other cut snapshot), or "next" (the plain docs/ tree, once versions.default is set - see Versioning). Rather than looping variables.versions by hand, call renderVersionSwitcher( variables.versions, variables.currentVersion ) - bound into every theme's render scope the same way themeAssetUrl()/ moduleAssetUrl() are (above) - and it builds the whole <select>, groups "next" behind a divider after every released version, and labels the "current" entry with "(Current)", all driven by kind rather than a theme re-implementing that logic itself. An optional second argument adds extra CSS classes to the <select> (renderVersionSwitcher( variables.versions, variables.currentVersion, "my-extra-class" )) for a theme that wants its own styling without reimplementing the markup. Every built-in theme gets its repo/social icons from a small shared SVG lookup, <bx:include template="#variables.moduleAssetsDir#/icons.bxm"> (defines bxsitesIcon( name ), one of github, twitter/x, rss, youtube, linkedin, facebook, bluesky, threads, slack, patreon, email, edit, clock, falling back to a generic link glyph)

  • a custom theme can include it the same way, or supply its own icons entirely.

A theme folder missing either required file fails fast with a clear BxSites.InvalidTheme error at build time, rather than a confusing template error deep inside rendering. blog.bxm/blog-page.bxm are never enforced - same "optional, falls back" shape search.bxm already has.

Multiple layouts per page

A theme can offer more than one outer shell and more than one body template, letting a blog (or any other content type) look different from the rest of the site while reusing the same theme's chrome and assets. Two independent resolution chains:

  • Outer shell - always layout.bxm, except the blog's own listing/ category/archive/author/stats pages, which use blog.bxm when the active theme has it. This one follows content type - there's no frontmatter key for it.
  • Body - page.bxm by default. A blog post uses blog-page.bxm when the active theme has it. Either way, a page's own frontmatter always wins when it sets one:
---
title: We raised a Series A
layout: press-release
---

layout: press-release renders this one page's body through .theme/press-release.bxm (or the active built-in theme's own, if it ships one) instead of page.bxm - still inside the site's normal layout.bxm shell. A layout: naming a file the active theme doesn't have falls back to page.bxm rather than failing the build, so switching themes never breaks a page that named one theme's own custom layout.

bootstrap ships blog.bxm/blog-page.bxm as a working example to copy; the other built-in themes don't yet, and fall back to layout.bxm/page.bxm for blog content the same way any incomplete .theme/ override would.

Which layout/body is active

Every .bxm a page renders through - layout.bxm, page.bxm, blog.bxm, blog-page.bxm, or a project's own custom one - can read which files actually resolved, the same bare way it already reads variables.page/ variables.data:

  • variables.layoutFile - the outer shell in use, e.g. "layout.bxm" or "blog.bxm"
  • variables.bodyFile - the body in use, e.g. "page.bxm", "blog-page.bxm", or a frontmatter-named one
  • variables.page.layout - the page's own raw frontmatter layout: value, if it set one; "" otherwise

Useful for a body class hook, or branching without a separate template:

<body class="layout-#reReplace( variables.bodyFile, '\.bxm$', '' )#">
<bx:if variables.bodyFile == "blog-page.bxm">
	<!-- blog-post-only chrome -->
</bx:if>

Customizing colors without a theme override

For a color/font tweak, forking a whole theme is overkill - each built-in theme reads its palette from a handful of CSS custom properties on :root, re-declared under [data-theme="dark"] for dark mode. bxsites.yaml's extraCss loads after the theme's own stylesheet, so a same-specificity re-declaration in it wins without touching resources/themes/ at all:

extraCss: [ assets/brand.css ]
{ "extraCss": ["assets/brand.css"] }
extraCss = [ "assets/brand.css" ]
/* docs/assets/brand.css - copied to site/assets/brand.css at build time */
:root {
	--bxsites-gradient-start: #7C3AED;
	--bxsites-gradient-end: #DB2777;
	--bxsites-accent: #FBBF24;
	--bxsites-link: #7C3AED;
	--bxsites-link-hover: #9F5AF0;
}

[data-theme="dark"] {
	--bxsites-link: #C4B5FD;
	--bxsites-link-hover: #DDD6FE;
}

The bootstrap theme's own set (resources/themes/bootstrap/assets/style.css) is --bxsites-gradient-start/-end, --bxsites-accent, --bxsites-bg, --bxsites-text, --bxsites-sidebar-bg, --bxsites-sidebar-text, --bxsites-border, --bxsites-link, --bxsites-link-hover, --bxsites-code-bg, --bxsites-step-marker-bg, --bxsites-step-marker-text, --bxsites-step-line, --bxsites-step-success-bg/-text, --bxsites-step-warning-bg/-text, --bxsites-step-danger-bg/-text and the --bxsites-table-* set that paints a rendered table's card, header strip, dividers and zebra/hover tints. Every built-in theme guarantees --bxsites-gradient-start/-end, --bxsites-accent, the --bxsites-step-* set and the --bxsites-table-* set under those exact names, so extraCss can always retarget the brand color/stepper accents regardless of theme - but only bootstrap, slate and notion also expose --bxsites-bg/-text/-sidebar-bg/-sidebar-text/-border/-link/-link-hover/-code-bg under those names (justthedocs aliases all but the two -sidebar-* ones the same way). Every other built-in theme (material, tailwind, docsy, docusaurus, vuepress, gitbook) uses its own internal custom-property names for that second group instead (e.g. material's own assets/style.css uses --md-bg/--md-ink/--md-link/...) - open that theme's own assets/style.css to find its real names before overriding one of those via extraCss. Anything beyond color/font (layout, adding/removing chrome) needs a real override or a custom theme - see below.

The rest back the ::: stepper/::: step directive block - --bxsites-step-marker-bg/-text are the default numbered circle's background/text color (bootstrap/material default it to the theme's own --bxsites-accent; tailwind uses a dedicated teal/mint pair since it doesn't have a single shared accent token), --bxsites-step-line is the connecting line between steps, and the -success/-warning/-danger pairs back a step's own optional color="..." attribute - unlike the default marker, these three are the same fixed bg/text pair in both light and dark mode (a self-contained badge, not tied to the theme's own brand accent), so there's no [data-theme="dark"] override to redeclare:

:root {
	--bxsites-step-marker-bg: #7C3AED;
	--bxsites-step-marker-text: #fff;
	--bxsites-step-success-bg: #059669;
	--bxsites-step-success-text: #fff;
}

[data-theme="dark"] {
	--bxsites-step-marker-bg: #C4B5FD;
	--bxsites-step-marker-text: #1b1f21;
}

Homepage hero banner

Every built-in theme ships CSS for a full-width homepage banner with a headline image and call-to-action buttons - this very site's own docs/index.md uses it. There's no directive block or config for it, just plain HTML any page can drop in (a homepage is just a normal page, order: 1 or otherwise first in nav):

<div class="bxsites-hero">
	<img class="bxsites-hero__banner" src="assets/home-banner.jpg" alt="...">
	<div class="bxsites-hero__actions">
		<a class="bxsites-hero__btn bxsites-hero__btn--primary" href="getting-started.md">Get Started</a>
		<a class="bxsites-hero__btn bxsites-hero__btn--secondary" href="https://github.com/your/repo">View on GitHub</a>
	</div>
</div>

bxsites-hero__btn--primary/--secondary are the same two accent styles every theme already uses elsewhere - swap, drop, or add buttons freely, and resize/replace bxsites-hero__banner's own image via a docs/assets/-relative src the same way any other image resolves.

Overriding a theme

Drop your own layout.bxm + page.bxm (and optionally search.bxm / assets/) into a .theme/ folder inside your project's content root - docs/.theme/, src/.theme/, or <source>/.theme/ for whatever source resolves to (see Content Source), not at the bare project root. BxSites prefers a content-root .theme/ override over both an installed .themes/<name>/ theme and any built-in theme, as long as it satisfies the contract below - the built-in themes under this module's own resources/themes/ are a good starting point to copy and adapt. Full resolution order: .theme/ (this section) -> .themes/theme.name/ (an installed theme, if theme.name matches one) -> a built-in theme named theme.name.

A worked example - start from bootstrap and swap its brand palette and heading font for your own, keeping everything else (nav, search, dark mode, code highlighting, ...) exactly as it already works:

my-project/
├── bxsites.yaml
└── docs/
    ├── index.md
    └── .theme/                    ← content-root override, checked before any built-in theme
        ├── layout.bxm              ← copied from resources/themes/bootstrap/layout.bxm
        ├── page.bxm                 ← copied from resources/themes/bootstrap/page.bxm, unchanged
        ├── search.bxm                ← copied unchanged
        └── assets/
            └── style.css               ← copied from bootstrap's assets/style.css, then edited
  1. Copy the three .bxm files and assets/style.css out of this module's resources/themes/bootstrap/ into your project's docs/.theme/ (or src/.theme/, or <source>/.theme/, matching wherever your content root actually is).
  2. Edit only what you need to change. To swap the brand palette and font, that's just the top of .theme/assets/style.css:
   :root {
   	--bxsites-gradient-start: #7C3AED;  /* was #00FF78 */
   	--bxsites-gradient-end: #DB2777;    /* was #00DBFF */
   	--bxsites-accent: #FBBF24;          /* was #FFF500 */
   }

   body {
   	font-family: "Inter", system-ui, sans-serif;  /* was "Poppins" */
   }
  1. Run bxSites build (or serve while iterating) - BxSites picks up .theme/ automatically, no bxsites.yaml change needed (a content-root .theme/ folder always takes precedence over the built-in theme named in theme.name). Everything you didn't touch - nav rendering, search, the dark-mode toggle, code annotations - keeps working exactly as it did in the original bootstrap theme, since it's still the exact same layout.bxm/page.bxm markup underneath.

A project's .theme/ folder is all-or-nothing, though - once BxSites finds one, it's used instead of the built-in theme entirely, so it still needs its own layout.bxm + page.bxm even if all you changed is assets/style.css (a folder missing either fails fast with BxSites.InvalidTheme rather than silently falling back). For a CSS-only/no-.bxm tweak, use extraCss above instead - it layers on top of whichever theme bxsites.yaml names, no .theme/ folder involved at all. .theme/ is for when you also need to change the markup itself, covered next.

Writing a theme from scratch

A theme only needs the two required files, so here's a genuinely minimal one - no Bootstrap/Tailwind, no dark mode, no search UI - to show exactly what's required versus what the built-in themes add on top. Save both as docs/.theme/layout.bxm and docs/.theme/page.bxm in your project (or under whichever content root your source resolves to) - a content-root .theme/ folder is picked up automatically (as above), no bxsites.yaml change needed:

<!-- docs/.theme/layout.bxm -->
<bx:script>
	function renderNav( required array nodes ) {
		var html = "<ul>"
		for ( var node in arguments.nodes ) {
			html &= "<li>"
			html &= len( node.url )
				? '<a href="' & variables.basePath & node.url & '">' & encodeForHTML( node.title ) & '</a>'
				: encodeForHTML( node.title )
			if ( node.children.len() ) {
				html &= renderNav( node.children )
			}
			html &= "</li>"
		}
		return html & "</ul>"
	}
</bx:script>
<bx:output>
<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>#encodeForHTML( variables.page.title )# - #encodeForHTML( variables.siteConfig.name )#</title>
	<link rel="stylesheet" href="#themeAssetUrl( 'style.css' )#">
</head>
<body>
	<header><a href="#variables.basePath#">#encodeForHTML( variables.siteConfig.name )#</a></header>
	<nav>#renderNav( variables.nav )#</nav>
	<main>
</bx:output>
<bx:include template="#variables.themeDir#/#variables.bodyFile#">
<bx:output>
	</main>
</body>
</html>
</bx:output>
<!-- docs/.theme/page.bxm -->
<bx:output>
<article>
	<h1>#encodeForHTML( variables.page.title )#</h1>
	#variables.page.contentHtml#
</article>
</bx:output>

That's a complete, working theme - variables.page.contentHtml is the already-converted markdown (syntax highlighting, admonitions, tabs, math and all), so there's nothing left to parse, only to lay out. From here, add whatever the built-in themes have that you actually want: search.bxm (only included when bxsites.yaml's search is true - see Search), a dark-mode toggle (copy the x-data/x-init Alpine.js pair off resources/themes/bootstrap/layout.bxm's <body> tag and the matching [data-theme="dark"] CSS block), breadcrumbs/tags/prev-next links (page.bxm in any built-in theme shows the pattern - each is just an if around a small render function, all driven by fields already present on variables.page), or an assets/ folder for your own CSS/JS, copied to site/assets/theme/ automatically at build time.

themeAssetUrl( "style.css" ) (used above, instead of a hardcoded assets/theme/style.css) is what makes your theme's own CSS pick up content-hash fingerprinting - assets.fingerprint (on by default) means every edit you make to that file gets a new filename on the next build, so visitors' browsers never serve a stale cached copy. It works the same way for any top-level CSS or JS file in your theme's own assets/ folder, not just style.css - a theme with its own assets/main.js links it the same way: <script src="themeAssetUrl( 'main.js' )"></script>. moduleAssetUrl( "copy-code.js" ) is the same helper for this module's own shared, theme-independent scripts (the ones every built-in theme's own layout.bxm links via variables.moduleAssetsDir) - use it instead of hardcoding assets/copy-code.js if your own theme links one directly.

Both helpers only fingerprint a file that sits directly inside that folder - never one nested in a subdirectory (your own assets/fonts/ or assets/vendor/, say). That's deliberate: a file referenced indirectly by one of your top-level files (a @font-face url(...), a CSS @import) would break if renamed without also rewriting every place that references it by its old name, so this never touches those. Calling either helper on a file that doesn't exist yet, or with assets.fingerprint off, falls back to today's exact plain path - never an error.

Any .bxm (built-in or custom) that hardcodes a plain path instead of calling one of these two helpers won't break, but it won't bust caches automatically either - and a build now prints a one-line, non-fatal warning naming the file when it detects exactly that, so a theme like this one doesn't go stale silently.

Edit this page Download Markdown Last updated Oct 7, 2026, 10:34:03 AM