automatic OS-driven dark mode on every web frontend (authd, buchinese, dashcam #17
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
2026-05-11 — cross-project: automatic OS-driven dark mode on every web frontend (authd, buchinese, dashcam, inventory, movement, nanodrop) — no in-app toggle, no preference persistence, purely
prefers-color-schememedia-query drivenUser wants every bchen.dev web app to follow the device's system theme automatically. When the OS (iOS, macOS, Android, Windows, Linux desktop) is in dark mode, the app renders in dark mode; when light, light. No toggle switch anywhere in the UI. No setting, no cookie, no localStorage, no per-account preference — the only input is
@media (prefers-color-scheme: dark). If the user changes their system theme, the app updates live (browsers re-evaluate the media query on theme change with no reload needed).Source request (verbatim from user): "file automatic dark mode as a feature request for every app. no need for a toggle switch anywhere, just have it be automatic based on device theming"
Apps in scope (one PR per app — these ship independently and don't share CSS):
~/authd/public/style.css+ login/account/oauth-consent HTML pages~/buchinese/public/style.css+ articles list/new/view HTML pages~/dashcam/public/style.css(single shared stylesheet, server-rendered viasrc/views/layout.ts)~/inventory/public/style.css+ all HTML pages underpublic/(login, index, item, items-new, oauth-clients, oauth-consent, api-tokens). Note: inventory already has/icon-color-scheme.jsfor the PWA home-screen icon swap — that's icon-only and unrelated; the CSS work is still needed.~/movement/src/client/styles/main.css+popover.css(and any other stylesheets undersrc/client/styles/)~/nanodrop/public/style.cssOut of scope this round (no web frontend, or N/A):
portman,tradebot,fleet(CLI/supervisor), theroles/docs.Implementation approach (apply to every app; details may vary by stylesheet):
<meta name="color-scheme" content="light dark">to every served HTML page's<head>. This tells the browser the page supports both modes and lets it pick sensible defaults for form controls, scrollbars, and the initial paint background before CSS loads (prevents the "white flash on dark OS" problem). In server-rendered layouts (dashcamsrc/views/layout.ts, movement client) add it there once; in static HTML files (authd/buchinese/inventory/nanodrop public/*.html) add to each file. Verify there is no existing<meta name="color-scheme" content="light">that would block this — if so, change it tolight dark.var(--name)references. Define the light-mode palette under:root { ... }and the dark-mode palette under@media (prefers-color-scheme: dark) { :root { ... } }. Minimum variable set per app (extend as the design needs):--bg(page background)--bg-elevated(cards, panels, modals)--fg(primary text)--fg-muted(secondary text, labels, hints)--border(dividers, input borders)--accent(primary buttons, links)--accent-fg(text on accent backgrounds)--danger(destructive buttons, error text) +--danger-fg--success(where used) +--success-fg--input-bg,--input-fg,--input-border)#000) on OLED looks dramatic but causes haloing around text; aim for a soft near-black like#0f1115or#111827for--bgand#1a1d23/#1f2937for--bg-elevated. Text should be off-white (#e5e7eb/#f3f4f6), not pure white. Accent color often needs a saturation/brightness bump in dark mode to keep contrast — e.g. an accent that's#2563ebon light might become#3b82f6on dark. Verify WCAG AA contrast (4.5:1 for body text, 3:1 for large text) for every color pair using a tool likehttps://webaim.org/resources/contrastchecker/ornpx wcag-contrast-check.input/textarea/select { font-size: 16px }rule from the global CLAUDE.md still applies and is orthogonal to dark mode; do NOT regress it. For dark mode specifically, set--input-bg,--input-fg,--input-borderand apply them. Browser-native controls (<input type="date">,<select>) will mostly take care of themselves oncecolor-scheme: light darkis in<meta>, but verify their popups look right on both iOS Safari and desktop Chrome.currentColorand let CSS color it viacolor: var(--fg).<picture><source media="(prefers-color-scheme: dark)" srcset="logo-dark.svg"><img src="logo-light.svg" /></picture>swap./icon-color-scheme.jsalready handles the home-screen icon swap on install; mirror that pattern in any other PWA that ships maskable icons (currently just inventory and nanodrop have manifests — checkpublic/manifest.webmanifest).@media (prefers-color-scheme: dark)inside HTML emails works on Apple Mail and Outlook.com but is unreliable elsewhere). Skip for this round; file as a follow-up if the user wants it.Anti-patterns to avoid:
<html class="dark">JS toggle. That's the Tailwind/Next.js default, but the user explicitly said no toggle and no persisted preference. Pure media query only.input,textarea,select { font-size: 16px }) when refactoring CSS — it must still compute to ≥16px on mobile for affected controls.<meta name="viewport" ... user-scalable=no>to suppress any iOS quirk — same accessibility reasoning as the font-size rule.Test plan per app:
/icon-color-scheme.jsalready covers the icon; verify it still works after the CSS refactor).popover.css) need verification — overlays on top of arbitrary page content are a common dark-mode breakage point.Acceptance per app PR:
npm run build/tsc --noEmitclean.getComputedStyle(document.body).backgroundColordiffers is a nice-to-have.Sequencing / coordination:
Out of scope (file as follow-ups if wanted):
manifest.webmanifestdeclares.Going-forward expectation: like the iOS focus-zoom rule in
~/.claude/CLAUDE.md, this is a one-off retrofit. After all six apps ship, any new web frontend the user builds should bake inprefers-color-schemesupport from day one. If this pattern becomes load-bearing, the reporter or user can promote it to a global rule in~/.claude/CLAUDE.mdor~/.claude/rules/common/coding-style.md(filing note: same writability caveat as the focus-zoom rule — the rules tree is owned bynobody:nogroupfrom sandbox sessions, so the rule may have to live in~/.claude/CLAUDE.mdinstead).Source: user via spawn-host chat (2026-05-11). Filed by reporter; spawn-host sessions do not edit project code directly per
~/CLAUDE.mdpolicy.