Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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-2…4 |
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.