Themes

The AkurAI Framework ships a named multi-theme system: 17 themes in dark and light variants, generated at request time by crates/css::theme into a single /themes.css stylesheet. Components never change — they read the same semantic token names (see Foundations); the active theme swaps the values under them.

Theme list

Themes are grouped by family. Each family ships a dark and a light variant.

| Slug | Family | Variant | |------|--------|---------| | akurai | AkurAI | dark | | akurai-light | AkurAI | light | | claude-code | Claude Code | dark | | claude-code-light | Claude Code | light | | nord | Nord | dark | | nord-light | Nord | light | | catppuccin-mocha | Catppuccin | dark | | catppuccin-latte | Catppuccin | light | | solarized-dark | Solarized | dark | | solarized-light | Solarized | light | | gruvbox-dark | Gruvbox | dark | | gruvbox-light | Gruvbox | light | | tokyo-night | Tokyo Night | dark | | tokyo-night-light | Tokyo Night | light | | rose-pine | Rosé Pine | dark | | rose-pine-dawn | Rosé Pine | light | | dracula | Dracula | dark |

The akurai theme is the default: its values are also emitted as the bare :root block, so a page with no data-theme attribute still renders correctly.

How it works

Themes are base16 colour schemes — 16 perceptual slots (base00base0F). The mapping from slots to framework tokens is polarity-agnostic: a base16 light scheme already inverts the base00base07 ramp, so the same mapping produces correct dark and light themes with no special-casing.

The themes_css() function in crates/css/src/theme.rs emits each theme as:

:root[data-theme="slug"]{
  --bg: …; --bg-2: …; --panel: …; --panel-2: …;
  --border: …; --border-2: …;
  --fg: …; --muted: …; --dim: …;
  --accent: …; --accent-2: …;
  --ok: …; --warn: …; --info: …; --danger: …;
  --shadow: …; color-scheme: dark | light;
}

Slot → token mapping

| base16 slot | Framework token(s) | |-------------|-------------------| | base00 | --bg | | base01 | --bg-2 | | base02 | --panel | | base03 | --panel-2, --border, --dim | | base04 | --border-2, --muted | | base05 | --fg | | base08 | --danger | | base0A | --warn | | base0B | --ok | | base0C | --info | | base0D | --accent | | base0E | --accent-2 |

The --shadow value and color-scheme are derived from the scheme's variant: field (dark or light), not from a palette slot.

Routes

/themes.css : The generated stylesheet. Load it after styles.css so theme token values win over any defaults in the base stylesheet.

/themes.json : The theme registry as JSON — an array of {slug, family, label, variant} objects, in picker order. The front-end switcher reads this at runtime so the picker is always in sync with the generated CSS.

The switcher (theme.js)

site/frontend/theme.js is a zero-dependency native-ESM module that:

  1. Reads the stored theme from localStorage (key akurai-theme) or falls

back to prefers-color-schemeakurai-light for light, akurai for dark.

  1. Applies the theme by setting data-theme on <html>.
  2. Persists changes back to localStorage.
  3. Mounts a grouped <select> picker into any [data-theme-picker] element,

fetching /themes.json to stay in sync with the generated CSS.

The switcher must load after the flash-guard (below) so the <html> attribute is set before the picker renders.

Using themes in a page

Full wiring (mirroring site/frontend/header.html)

Add these to <head>, in this order:

<!-- 1. Base component styles -->
<link rel="stylesheet" href="/styles.css" />

<!-- 2. Theme token overrides — must come after styles.css -->
<link rel="stylesheet" href="/themes.css" />

<!-- 3. Flash-guard: set the active theme before first paint (inline, sync) -->
<script>
  (function () {
    try {
      var t = localStorage.getItem("akurai-theme");
      if (!t) t = matchMedia("(prefers-color-scheme: light)").matches
        ? "akurai-light"
        : "akurai";
      document.documentElement.setAttribute("data-theme", t);
    } catch (e) {}
  })();
</script>

<!-- 4. Switcher module — mounts pickers and handles changes -->
<script type="module" src="/theme.js"></script>

Picker slot

Place an empty element with data-theme-picker anywhere in the header. The switcher finds it on DOMContentLoaded and fills it with the grouped <select>:

<span class="theme-picker" data-theme-picker></span>

Manual theme control

Use applyTheme(slug) from theme.js to switch themes from your own code:

import { applyTheme } from "/theme.js";
applyTheme("nord");

Adopting themes in a standalone app

If you are building an app outside the framework and want to use the same themes.css:

  1. Vendor themes.css from the framework's built binary (served at

/themes.css on any running instance) or regenerate it with crates/css::theme::themes_css().

  1. Alias your local tokens to the framework tokens using lazy var():
   :root {
     --color-bg: var(--bg);
     --color-text: var(--fg);
     --color-primary: var(--accent);
   }

This way your components read your tokens, but the values come from whatever theme is active.

  1. Derive tints with color-mix rather than hardcoding hex:
   background: color-mix(in srgb, var(--ok) 10%, var(--panel));
  1. Copy the flash-guard inline script and theme.js (or re-implement the

three-step: read storage → set data-theme → listen for changes).

Adding a new theme

  1. Vendor a base16 scheme. Drop a .yaml file into

crates/css/src/schemes/. The parser reads name:, variant:, and base00base0F. See akurai.yaml as a reference — comments after # are ignored, values may be quoted or bare.

  1. Register it. Add one line to the SCHEMES constant in

crates/css/src/theme.rs:

   Entry {
       slug: "my-theme",
       family: "My Family",
       raw: include_str!("schemes/my-theme.yaml"),
   },

The slug becomes the data-theme value and the /themes.json key. The family groups it in the picker. That is the entire change — no other code touches theme registration.

  1. Rebuild. The next akurai serve regenerates /themes.css and

/themes.json from the updated SCHEMES list at request time.