AkurAI Build
Menu

Public repository / 91bd2a37654909c5a3aef6ddd614991bd378ccb8

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.