Files
heartbeat/docs/DARK_MODE.md
T
Andreas Wrede 37b8e35a26 docs: add DARK_MODE.md
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 22:34:59 -04:00

2.8 KiB

Dark Mode

Every page in the Heartbeat web UI supports light mode, dark mode, and automatic (follows the OS/browser setting). Each user picks their preference independently; it is stored in the browser and takes effect immediately without a page reload.


Choosing a theme

Open your profile page (/profile) and scroll to the Appearance section. Click one of the three buttons:

Button Behaviour
Auto Follows the OS or browser dark-mode preference. Updates live if the system setting changes.
Light Always light, regardless of system setting.
Dark Always dark, regardless of system setting.

The preference is stored in localStorage under the key hbd_theme and applies to the current browser only. Clearing browser storage resets it to Auto.


Implementation notes

No flash of unstyled content

A small synchronous <script> runs at the very top of <head>, before any CSS is parsed, and sets data-theme="dark" on <html> when the stored preference (or the system setting in auto mode) calls for dark. Because it runs before paint, there is no visible flicker on page load.

CSS custom properties

All colours are expressed as CSS custom properties defined in head.html:

:root                    — light-mode values (default)
html[data-theme="dark"]  — dark-mode overrides

Key variables:

Variable Purpose
--bg Page background
--surface Card / panel background
--surface-2 / --surface-3 Slightly lighter/darker surfaces (table rows, hover states)
--text / --text-sec / --text-muted Primary, secondary, muted text
--border / --border-24 Border shades from prominent to faint
--link Hyperlink and interactive-element colour
--nav-bg Navigation bar background
--input-bg / --input-border Form control colours
--shadow / --shadow-sm Box-shadow alphas

A single global rule in head.html themes all <input>, <select>, and <textarea> elements across every page at once:

html[data-theme="dark"] input:not([type=checkbox]):not([type=radio]),
html[data-theme="dark"] select,
html[data-theme="dark"] textarea {  }

Each page template adds its own html[data-theme="dark"] block for page-specific elements (cards, tables, badges, etc.).

Auto-mode live updates

A matchMedia change listener in head.html updates data-theme whenever the OS preference changes, so users in Auto mode see the theme switch without reloading.

Semantic colours are unchanged

Alert colours (red for critical, orange for warning, green for ok) and status indicators are intentionally left as fixed values — they are semantic signals, not surface colours, and look correct on both light and dark backgrounds.