THEMES.md still described the pre-#627 model where each theme was a
monolithic palette name (Dark, Light, Slate, Solarized Dark, Monokai,
Nord, OLED). The current architecture splits appearance into two
orthogonal pickers:
- Theme (System / Dark / Light) — applied as `.dark` class on <html>
- Skin (8 named accent palettes) — applied as `data-skin` attribute
Rewrite the doc to:
- Open with the Theme × Skin separation and how they combine
- List the 3 themes and 8 actual skins shipped in static/style.css
(default, ares, mono, slate, poseidon, sisyphus, charizard, sienna),
with the same descriptive tone as the original
- Replace "Creating a Custom Theme" with "Creating a Custom Skin" as
the primary extension point, with paired light + dark CSS variants
- Note the WebUI extensions surface (docs/EXTENSIONS.md) as a
no-fork path for self-hosted custom skins
- Update internals to reflect classList.toggle('dark') + dataset.skin
+ dataset.fontSize instead of the old data-theme-only model
- Add a brief Font Size section since it sits in the same picker
- Keep a smaller Custom Theme section for the rare case someone wants
to override the core palette, redirecting most users to skins
Docs-only change; no code touched.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
6.5 KiB
Hermes Web UI — Themes
Hermes Web UI splits appearance into two independent pickers:
- Theme — the mode:
System,Dark, orLight. Drives the background, text, surface, and chrome colors. - Skin — the accent palette: eight named skins ship built-in. Drives only
the
--accentfamily (active states, links, focus rings, primary actions).
You pick one of each and they combine, so the look adapts to your environment without losing your favorite accent — pure CSS, no Python changes needed.
Switching Appearance
Settings panel: Click the gear icon → Appearance. The Theme card toggles Light/Dark/System; the Skin grid offers eight accent palettes. Preview is instant — the UI updates as you click.
Slash command: Type /theme <name> in the composer. The command accepts
both theme names (system, dark, light) and skin names (default, ares,
mono, slate, poseidon, sisyphus, charizard, sienna). It updates the
matching axis and leaves the other one alone.
Persistence: Both choices are stored in localStorage for flicker-free
loading, and saved server-side via POST /api/settings (under theme and
skin keys in settings.json).
Built-in Themes
| Theme | Description |
|---|---|
| System (default) | Follows the OS prefers-color-scheme preference and updates live. |
| Dark | Deep dark surfaces, low-glare for long sessions. |
| Light | Bright surfaces with dark text, high contrast for daylight environments. |
The theme is applied as a class on <html>: .dark is present for dark mode,
absent for light. System mode tracks the OS preference at runtime.
Built-in Skins
| Skin | Description |
|---|---|
| Default | The original Hermes gold accent. Warm and understated. |
| Ares | Fiery red. High-energy and assertive. |
| Mono | Neutral gray. Distraction-free, for deep focus. |
| Slate | Slate blue-gray. Subtle and grown-up. |
| Poseidon | Ocean blue. Calm and focused for long sessions. |
| Sisyphus | Vivid purple. Distinctive without being loud. |
| Charizard | Warm orange. Energetic and easy on the eyes. |
| Sienna | Warm clay and sand earth palette. Soft and natural. |
Each skin defines paired light + dark variants so it reads cleanly on either
theme. The skin is applied as data-skin="<name>" on <html> (the default
skin clears the attribute).
Creating a Custom Skin
A skin is a small CSS block that overrides the accent variables for both the light and dark variants:
/* Light variant */
:root[data-skin="my-skin"] {
--accent: #2E7D32; /* Active states, links, primary buttons */
--accent-hover: #1B5E20; /* Hover */
--accent-bg: rgba(46,125,50,0.08); /* Soft tinted backgrounds */
--accent-bg-strong: rgba(46,125,50,0.15); /* Highlighted backgrounds */
--accent-text: #1B5E20; /* Text on accent bg */
}
/* Dark variant — usually lighter or more saturated for contrast */
:root.dark[data-skin="my-skin"] {
--accent: #66BB6A;
--accent-hover: #43A047;
--accent-bg: rgba(102,187,106,0.08);
--accent-bg-strong: rgba(102,187,106,0.15);
--accent-text: #66BB6A;
}
Two ways to ship it:
-
In the repo (built-in): add the block to
static/style.css, register it in the Settings skin picker (static/index.html) and in the/themecommand list (static/commands.js), then open a PR. -
Self-hosted (no fork): use the WebUI extensions surface — see
docs/EXTENSIONS.md. Drop your CSS inHERMES_WEBUI_EXTENSION_DIRand declare it inHERMES_WEBUI_EXTENSION_STYLESHEET_URLS. No code changes needed; the skin attribute can be set from your own JS.
Tips
- Test both themes. A skin that pops on Dark can be illegible on Light.
Always check
:root[data-skin](light) and:root.dark[data-skin](dark). - Pick contrasting
--accent-texton--accent-bg. The strong variant appears behind small labels and chips; weak contrast there reads as blur. - The logo gradient uses
--accentautomatically, so it adapts to your skin without any extra work. - No server changes needed. The
skinsetting insettings.jsonaccepts any string, so your custom skin name persists without code changes once you load the CSS.
Creating a Custom Theme
A full custom theme (a different overall mood, not just an accent change) is
a larger task than a skin: it has to redefine the core palette variables
(--bg, --surface, --text, --border, --code-bg, and friends) for one
or both modes. The contract is defined in the top :root and :root.dark
blocks of static/style.css — start there.
Most of the time, a custom skin is what you actually want. Reach for a custom theme only when the existing Light/Dark modes don't fit (for example, a high-contrast accessibility theme or an OLED black variant).
Font Size
Right under Theme/Skin in Settings → Appearance: Small, Default,
Large. Applied as data-font-size on <html> and scales the WebUI's root
font size. Persists alongside theme and skin.
How It Works Internally
- Theme:
document.documentElement.classList.toggle('dark', isDark)— light mode removes the class. System mode tracksmatchMedia('(prefers-color-scheme: dark)'). - Skin:
document.documentElement.dataset.skin = name(or remove the attribute fordefault). - Font size:
document.documentElement.dataset.fontSize = size(or remove fordefault). - No flash on load: a tiny inline
<script>in<head>readslocalStoragebefore the stylesheet does, so the right look is applied before paint. - Server sync: preferences are saved via
POST /api/settingsand rehydrated on boot viaGET /api/settings.
Contributing a Skin
Skins are the easiest extension point — pure CSS, no Python, no JS logic. To contribute one upstream:
- Add your
:root[data-skin="name"]and:root.dark[data-skin="name"]blocks tostatic/style.css. - Register it in the Settings skin picker in
static/index.htmland in the skin list used bycmdTheme()instatic/commands.js. - Test on desktop and mobile across both Light and Dark themes.
- Open a PR — skins are pure CSS additions with no backend changes needed.
For a custom theme (overriding the base palette), prefer opening an issue first to discuss scope, since it touches many selectors.