6.8 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: built-in skins ship as named keys. 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 the built-in 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,
catppuccin, nous, geist-contrast). 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. |
| Catppuccin | Catppuccin Latte/Mocha palette with Mauve accent. |
| Nous | Steel-blue accent with dashed technical surfaces. |
Geist Contrast (geist-contrast) |
Geist-inspired monochrome surfaces with a restrained dark-mode #FFF175 accent. |
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.