Add persistent Dark Mode with System/Light/Dark selector and foreground token system#49
Add persistent Dark Mode with System/Light/Dark selector and foreground token system#49
Conversation
- Add useColorMode hook (src/lib/colorMode.ts) with light|dark|system preference, localStorage persistence under roflbox.colorMode, and prefers-color-scheme system fallback - Add FOUC-prevention inline script to index.html - Add [data-color-mode] CSS tokens (--text-primary, --surface-1, etc.) plus Shadcn variable overrides for light mode in index.css - Add keyboard-accessible Dark mode toggle (role=switch, aria-label, aria-checked) to BackgroundSwitcher settings panel with (system) hint - Wire useColorMode into App.tsx and pass props to BackgroundSwitcher Co-authored-by: zealsprince <1859270+zealsprince@users.noreply.github.com>
zealsprince
left a comment
There was a problem hiding this comment.
@copilot Thanks — this is the right direction and the scope is aligned to issue #22. I’m requesting one more pass before approval so we keep the behavior crystal-clear and robust.
Please address the following:
- Draft status / ship readiness
- Once the checklist below is satisfied, mark the PR ready for review (or confirm why it should remain draft).
- Toggle semantics: make the UI reflect the real 3-state model
- The implementation persists
light | dark | system, but the UX is described as a simple dark mode toggle. - Please make the displayed state unambiguous for users. Either:
- keep the switch but add adjacent text that clearly communicates when the app is currently following
system, including whether the effective mode is light or dark, OR - provide a compact explicit selector for
System / Light / Darkin the panel.
- keep the switch but add adjacent text that clearly communicates when the app is currently following
- Main goal: avoid a confusing case where a binary switch is shown while the persisted setting is actually
system.
- Manual verification note in PR body/comment
- Please add a short verification checklist confirming you manually tested:
- first visit with OS dark preference
- first visit with OS light preference
- switching explicitly to dark, then refresh
- switching explicitly to light, then refresh
- returning to system mode
- keyboard-only interaction and visible focus
- background preset changes while in each color mode
- FOUC / hydration resilience
- Please confirm the inline bootstrap script safely handles invalid localStorage values and absence of
matchMediawithout throwing. - If not already handled, harden it so bad stored values fall back to
systemsafely.
- Accessibility wording
- Keep
aria-label="Toggle dark mode"if you stay with a switch, but ensure any supplemental visible label makes the current effective state clear.
Once those are addressed, this looks close. I do not want to approve yet because approval will auto-merge and I want the system-vs-toggle UX ambiguity resolved first.
- Replace ambiguous binary switch with a compact 3-button radiogroup (System | Light | Dark) so the persisted 3-state model is always visually unambiguous - Show "effective: light/dark" subtitle when System is selected so users know the currently active mode - Aria: role="radiogroup", role="radio", aria-checked, and aria-label that includes current effective mode for System button - Harden FOUC inline script: wrap in try/catch, guard matchMedia with typeof check to safely handle missing API or invalid stored values - Remove unused isDark helper and handleDarkModeToggle Co-authored-by: zealsprince <1859270+zealsprince@users.noreply.github.com>
All points addressed in b54af17. 1. 3-state selector
2. FOUC / hydration resilience try {
var saved = localStorage.getItem('roflbox.colorMode');
if (saved === 'light' || saved === 'dark') { mode = saved; }
else if (typeof window.matchMedia === 'function' && matchMedia('(prefers-color-scheme: dark)').matches) { mode = 'dark'; }
else { mode = 'light'; }
document.documentElement.setAttribute('data-color-mode', mode);
} catch (e) { /* leave attribute unset; CSS :root defaults apply */ }Invalid/unexpected stored values fall through to the 3. Accessibility Manual verification checklist
|


Background theming alone leaves cards, text, and borders harsh when users want a calmer UI. This adds a cohesive
light/dark/systemcolor mode layer that controls foreground surfaces independently of the existing background preset.New:
src/lib/colorMode.tsuseColorModehook — manages'light' | 'dark' | 'system'preferencelocalStorageunderroflbox.colorMode; defaults tosystem(respectsprefers-color-scheme)data-color-mode="light|dark"ondocument.documentElementsystemmodeCSS tokens (
src/index.css)Two new attribute-selector blocks, placed after all
[data-bg-theme]rules so cascade order guarantees color mode wins without specificity hacks:All Shadcn components (
Card,Button, etc.) adapt automatically via their existingbg-card,text-card-foreground,text-muted-foregroundclasses.Settings panel (
BackgroundSwitcher.tsx)System | Light | Dark3-buttonradiogroupbelow the background presets listSystemis selected, aneffective: light/darksubtitle shows the currently resolved value so the active mode is always unambiguousrole="radiogroup",role="radio",aria-checked, andaria-labelon each button (System button includes current effective mode, e.g."System color mode (currently dark)")FOUC prevention (
index.html)Inline script applies
data-color-modebefore React hydrates — hardened withtry/catchand atypeof matchMedia === 'function'guard so invalid stored values and environments withoutmatchMediafall back gracefully without throwing.Screenshots
Manual Verification
Systemselected,effective: dark)Systemselected,effective: light)Darkexplicitly →localStorage = "dark", persists after refreshLightexplicitly →localStorage = "light", persists after refreshSystem→localStorage = "system", follows OS after refresh;effective:subtitle reflects resolved valueSystem / Light / Dark, Enter/Space activate; focus ring visible in both modesOriginal prompt
🔒 GitHub Advanced Security automatically protects Copilot coding agent pull requests. You can protect all pull requests by enabling Advanced Security for your repositories. Learn more about Advanced Security.