Skip to content

Add persistent Dark Mode with System/Light/Dark selector and foreground token system#49

Open
Copilot wants to merge 3 commits intomainfrom
copilot/add-dark-mode-toggle
Open

Add persistent Dark Mode with System/Light/Dark selector and foreground token system#49
Copilot wants to merge 3 commits intomainfrom
copilot/add-dark-mode-toggle

Conversation

Copy link
Contributor

Copilot AI commented Mar 6, 2026

Background theming alone leaves cards, text, and borders harsh when users want a calmer UI. This adds a cohesive light/dark/system color mode layer that controls foreground surfaces independently of the existing background preset.

New: src/lib/colorMode.ts

  • useColorMode hook — manages 'light' | 'dark' | 'system' preference
  • Persists in localStorage under roflbox.colorMode; defaults to system (respects prefers-color-scheme)
  • Writes data-color-mode="light|dark" on document.documentElement
  • Reacts to OS-level changes while in system mode

CSS 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:

[data-color-mode="dark"]  { --text-primary: …; --surface-1: …; --border-color: …; … }
[data-color-mode="light"] { --text-primary: …; --surface-1: …; …
  /* also overrides all Shadcn HSL tokens: --card, --foreground, --border, etc. */
}

All Shadcn components (Card, Button, etc.) adapt automatically via their existing bg-card, text-card-foreground, text-muted-foreground classes.

Settings panel (BackgroundSwitcher.tsx)

  • Replaced ambiguous binary toggle with a compact System | Light | Dark 3-button radiogroup below the background presets list
  • When System is selected, an effective: light/dark subtitle shows the currently resolved value so the active mode is always unambiguous
  • Full ARIA: role="radiogroup", role="radio", aria-checked, and aria-label on each button (System button includes current effective mode, e.g. "System color mode (currently dark)")
  • All three buttons are keyboard operable with visible focus rings in both modes

FOUC prevention (index.html)

Inline script applies data-color-mode before React hydrates — hardened with try/catch and a typeof matchMedia === 'function' guard so invalid stored values and environments without matchMedia fall back gracefully without throwing.

Screenshots

System mode (effective: light) Dark selected
system dark

Manual Verification

  • ✅ OS dark preference + no localStorage → loads dark (System selected, effective: dark)
  • ✅ OS light preference + no localStorage → loads light (System selected, effective: light)
  • ✅ Select Dark explicitly → localStorage = "dark", persists after refresh
  • ✅ Select Light explicitly → localStorage = "light", persists after refresh
  • ✅ Select SystemlocalStorage = "system", follows OS after refresh; effective: subtitle reflects resolved value
  • ✅ Keyboard: Tab to radiogroup, arrow keys cycle System / Light / Dark, Enter/Space activate; focus ring visible in both modes
  • ✅ Switching background presets (Classic ROFL, Paper White, Hacker Green, Deep Space) while in Dark or Light mode — cards/text remain readable in every combination
Original prompt

This section details on the original issue you should resolve

<issue_title>Add Dark Mode (UI theme) toggle for foreground styling (not just background)</issue_title>
<issue_description>## Context
User feedback now explicitly requests a Dark Mode toggle in addition to toning down the background.

We already have background theming issues (#10, #12, #16#21) that mainly focus on page background. Dark Mode is broader: it should also adjust foreground surfaces (cards), text, borders, and button styles for comfortable prolonged use.

Problem

Even with calmer backgrounds or a background switcher, the site can still feel harsh if text/cards are not tuned for a darker UI. Users expect a cohesive dark theme where the entire UI adapts.

Goal

Ship a simple, accessible Dark Mode toggle that switches the site’s overall UI theme (foreground colors + surfaces), integrates with the existing theming foundation, and persists.

Requirements

UX

  • Add a small toggle in the same top-right settings/panel as the background switcher.
    • Label: Dark mode
    • Control: switch/toggle
  • Default behavior:
    • If user has never chosen: follow prefers-color-scheme (system).
    • If user chooses explicitly: persist and override system.

Behavior

  • When Dark Mode is on:
    • Background can remain user-selected, but cards/surfaces/text should adapt.
    • Ensure readable contrast for headings, body text, links, and buttons.
  • When off:
    • Return to light theme tokens.

Technical

  • Implement via data-color-mode="light|dark" on document.documentElement.
  • Define core CSS tokens for surfaces/foreground:
    • --text-primary, --text-secondary
    • --surface-1 (page content cards), --surface-2 (panels)
    • --border-color
    • --button-bg, --button-text (or reuse existing component styles)
  • Persistence:
    • localStorage.roflbox.colorMode = "light"|"dark"|"system"
    • On load: apply stored value; if system, use media query.
  • Respect reduced motion (no required animated transitions).

Accessibility

  • Toggle is keyboard operable.
  • Add aria-label="Toggle dark mode".
  • Ensure focus states are visible in both modes.

Acceptance Criteria

Related

Test Plan

  • Manual: system set to dark -> first visit loads dark.
  • Manual: toggle dark mode -> persists after refresh.
  • Manual: switch backgrounds while in dark mode -> text/cards remain readable.
  • Manual: keyboard navigate toggle + panel controls.
    </issue_description>

Comments on the Issue (you are @copilot in this section)

@zealsprince Prioritizing this issue based on repeated site feedback: the background remains too visually intense and readability is still the main complaint. This Dark Mode toggle for foreground UI is the highest-value next step because it improves cards, text, buttons, and overall comfort across all existing background presets.

@copilot Please take this next if/when assignment automation is unavailable. Keep scope focused on:

Please keep the implementation narrowly scoped to the UI theme toggle and include a short manual test checklist in the PR description.</comment_new>


🔒 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.

- 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>
Copilot AI changed the title [WIP] Add dark mode toggle for overall UI theme Add persistent Dark Mode toggle with foreground token system Mar 6, 2026
Copy link
Member

@zealsprince zealsprince left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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:

  1. Draft status / ship readiness
  • Once the checklist below is satisfied, mark the PR ready for review (or confirm why it should remain draft).
  1. 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 / Dark in the panel.
  • Main goal: avoid a confusing case where a binary switch is shown while the persisted setting is actually system.
  1. 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
  1. FOUC / hydration resilience
  • Please confirm the inline bootstrap script safely handles invalid localStorage values and absence of matchMedia without throwing.
  • If not already handled, harden it so bad stored values fall back to system safely.
  1. 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>
Copy link
Contributor Author

Copilot AI commented Mar 6, 2026

@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 ...

All points addressed in b54af17.

1. 3-state selector
Replaced the binary switch with a compact System | Light | Dark radiogroup. When System is selected, an effective: light/dark subtitle shows the current resolved value so there's no ambiguity.

System (effective: light) Dark selected
system dark

2. FOUC / hydration resilience
The inline script now guards every failure path:

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 matchMedia check; any environment where localStorage or matchMedia throws is caught silently.

3. Accessibility
Each option is role="radio" with aria-checked; the group is role="radiogroup" aria-label="Color mode". The System button's aria-label includes the current effective mode: "System color mode (currently light)". All three buttons show a visible focus ring.

Manual verification checklist

  • ✅ OS dark preference + no localStorage → loads dark (System selected, effective: dark)
  • ✅ OS light preference + no localStorage → loads light (System selected, effective: light)
  • ✅ Select Dark explicitly → localStorage = "dark", refreshes to dark
  • ✅ Select Light explicitly → localStorage = "light", refreshes to light
  • ✅ Select System → localStorage = "system", follows OS after refresh; effective: subtitle shows resolved value
  • ✅ Keyboard: Tab to radiogroup, arrow keys cycle System/Light/Dark, Enter/Space activate
  • ✅ Focus ring visible on all three buttons in both dark and light mode
  • ✅ Switching background presets (Classic ROFL, Paper White, Hacker Green, Deep Space) while in Dark or Light mode — cards/text remain readable in each combination

Copilot AI changed the title Add persistent Dark Mode toggle with foreground token system Add persistent Dark Mode with System/Light/Dark selector and foreground token system Mar 6, 2026
@zealsprince zealsprince marked this pull request as ready for review March 6, 2026 19:34
@zealsprince zealsprince enabled auto-merge (squash) March 6, 2026 19:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add Dark Mode (UI theme) toggle for foreground styling (not just background)

2 participants