How It Works

This page explains the technical implementation of the dark theme.

Overview

The theme uses Antora’s supplemental UI feature to overlay additional files onto the default UI bundle. This approach means you don’t need to fork or rebuild the Antora Default UI.

The Four Key Files

1. head-meta.hbs (FOUC Prevention)

This Handlebars partial is injected into the <head> of every page. It does two critical things:

  1. Loads the site-extra.css stylesheet

  2. Runs an inline script that immediately applies dark mode if needed

<link rel="stylesheet" href="{{{uiRootPath}}}/css/site-extra.css">
<script>
(function () {
  const themeKey = 'antora-theme';
  const savedTheme = localStorage.getItem(themeKey);
  if (savedTheme === 'dark' || (!savedTheme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
    document.documentElement.classList.add('dark-theme');
  }
})();
</script>

The inline script runs before the page renders, preventing the flash of light mode that would occur if we waited for the main JavaScript to load.

This partial is injected at the end of the page body. It loads:

  • The main Antora site script

  • highlight.js for syntax highlighting

  • The dark mode toggle script

3. site-dark-mode.js (Toggle Logic)

This JavaScript module handles:

  • Button injection - Creates and inserts the toggle button into .navbar-end

  • Click handling - Switches between light and dark themes

  • Persistence - Saves preference to localStorage

  • Icon updates - Shows sun icon in dark mode, moon icon in light mode

Key functions:

// Apply theme and update button icon
function applyTheme(theme) {
  if (theme === 'dark') {
    document.documentElement.classList.add('dark-theme');
  } else {
    document.documentElement.classList.remove('dark-theme');
  }
  updateButtonIcon(theme);
  localStorage.setItem('antora-theme', theme);
}

// Toggle between themes
function toggleTheme() {
  const isDark = document.documentElement.classList.contains('dark-theme');
  applyTheme(isDark ? 'light' : 'dark');
}

4. site-extra.css (Dark Styles)

All dark mode styles use the selector prefix html.dark-theme:

html.dark-theme body {
  background-color: #1a1b1e;
  color: #c1c2c5;
}

html.dark-theme .navbar {
  background-color: #141517;
}

This explicit selector approach works because the Antora Default UI’s compiled CSS doesn’t use CSS custom properties for theming. Our dark styles simply override the default light styles when the dark-theme class is present.

Why This Approach?

No UI Bundle Modification

The standard way to customize Antora’s appearance is to fork the Antora Default UI and rebuild the UI bundle. This works but has downsides:

  • You must maintain your fork

  • UI updates require manual merging

  • Build complexity increases

The supplemental UI approach avoids all of this. Your dark theme overlays the standard UI bundle, and you can update the UI bundle independently.

CSS Specificity

Dark mode styles use html.dark-theme as a prefix, which provides enough specificity to override the default UI styles without using !important.

No JavaScript Framework

The toggle script is vanilla JavaScript with no dependencies. It’s small (~2KB) and works in all modern browsers.

Sequence of Events

When a page loads:

  1. Browser starts parsing HTML

  2. <head> is processed, loading head-meta.hbs

  3. Inline script checks localStorage and system preference

  4. If dark mode should be active, dark-theme class is added to <html> immediately

  5. CSS loads with dark mode already applied (no flash)

  6. Page renders in the correct theme

  7. Footer scripts load, including site-dark-mode.js

  8. Toggle button is injected into navbar

  9. User can now toggle between themes