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 (base00–base0F). The mapping from slots to framework tokens is polarity-agnostic: a base16 light scheme already inverts the base00→base07 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:
- Reads the stored theme from
localStorage(keyakurai-theme) or falls
back to prefers-color-scheme — akurai-light for light, akurai for dark.
- Applies the theme by setting
data-themeon<html>. - Persists changes back to
localStorage. - 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:
- Vendor
themes.cssfrom the framework's built binary (served at
/themes.css on any running instance) or regenerate it with crates/css::theme::themes_css().
- 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.
- Derive tints with
color-mixrather than hardcoding hex:
background: color-mix(in srgb, var(--ok) 10%, var(--panel));
- 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
- Vendor a base16 scheme. Drop a
.yamlfile into
crates/css/src/schemes/. The parser reads name:, variant:, and base00–base0F. See akurai.yaml as a reference — comments after # are ignored, values may be quoted or bare.
- Register it. Add one line to the
SCHEMESconstant 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.
- Rebuild. The next
akurai serveregenerates/themes.cssand
/themes.json from the updated SCHEMES list at request time.