Menu
Public repository / 439f0dcabf5cdf2b120c7724be895cf1df30507f
AkurAI-Framework / site/content/docs/design-system/themes.md
Latest change d3aacc97a3d97591d3118c06cb7678a33241191b - docs: document v0.8.0 named theme system by Ólafur Búi Ólafsson
# 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](/docs/design-system/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](https://github.com/tinted-theming/schemes) 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:
```css
: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-scheme` — `akurai-light` for light, `akurai` for dark.
2. **Applies** the theme by setting `data-theme` on `<html>`.
3. **Persists** changes back to `localStorage`.
4. **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:
```html
<!-- 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>`:
```html
<span class="theme-picker" data-theme-picker></span>
```
### Manual theme control
Use `applyTheme(slug)` from `theme.js` to switch themes from your own code:
```js
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()`.
2. **Alias your local tokens** to the framework tokens using lazy `var()`:
```css
: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.
3. **Derive tints** with `color-mix` rather than hardcoding hex:
```css
background: color-mix(in srgb, var(--ok) 10%, var(--panel));
```
4. **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
`base00`–`base0F`. See `akurai.yaml` as a reference — comments after `#` are
ignored, values may be quoted or bare.
2. **Register it.** Add one line to the `SCHEMES` constant in
`crates/css/src/theme.rs`:
```rust
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.
3. **Rebuild.** The next `akurai serve` regenerates `/themes.css` and
`/themes.json` from the updated `SCHEMES` list at request time.