Skip to content

feat(web): add light/dark theme toggle#1715

Open
fezzzza wants to merge 2 commits into
coleam00:devfrom
fezzzza:feat/web-light-mode
Open

feat(web): add light/dark theme toggle#1715
fezzzza wants to merge 2 commits into
coleam00:devfrom
fezzzza:feat/web-light-mode

Conversation

@fezzzza
Copy link
Copy Markdown

@fezzzza fezzzza commented May 18, 2026

Summary

  • Problem: Web UI only supports dark mode — no way for users to switch to a light theme
  • Why it matters: Accessibility and user preference — many users prefer light themes, especially in bright environments
  • What changed: Added a theme toggle button in the top navigation bar with localStorage persistence and system preference detection
  • What did not change: No changes to server, adapters, or any backend packages

UX Journey

Before

User opens Web UI → sees dark theme only → no option to change

After

User opens Web UI → detects system preference (light/dark)
User clicks toggle in top nav → theme switches immediately
Preference persisted in localStorage → survives page reload

Architecture Diagram

Before

TopNav (no theme controls)

After

[+] useTheme.ts hook (system preference detection + localStorage persistence)
[~] TopNav.tsx (theme toggle button)
[~] index.css (light theme CSS variables)
[~] index.html (dark class on <html>)
From To Status Notes
useTheme TopNav new hook provides toggle state
useTheme localStorage new persists preference
useTheme window.matchMedia new detects system preference

Label Snapshot

  • Risk: risk: low
  • Size: size: XS
  • Scope: web
  • Module: web:layout

Change Metadata

  • Change type: feature
  • Primary scope: web

Linked Issue

  • Related to community requests for light mode support

Validation Evidence (required)

bun run validate

All checks pass: type-check, lint, format, tests.

  • Evidence provided: Manual testing — toggled between light/dark themes, reloaded page, confirmed persistence. Verified system preference detection works.

Security Impact (required)

  • New permissions/capabilities? No
  • New external network calls? No
  • Secrets/tokens handling changed? No
  • File system access scope changed? No

Compatibility / Migration

  • Backward compatible? Yes
  • Config/env changes? No
  • Database migration needed? No

Human Verification (required)

  • Verified scenarios: Theme toggle click, page reload persistence, system preference detection, fallback to dark when no preference
  • Edge cases checked: localStorage unavailable (restricted context) — wrapped in try-catch
  • What was not verified: SSR environments (not applicable to Vite SPA)

Side Effects / Blast Radius (required)

  • Affected subsystems: Web UI only
  • Potential unintended effects: None — CSS variable approach is additive
  • Guardrails: Default to dark theme if anything fails

Rollback Plan (required)

  • Fast rollback: revert this PR
  • No feature flags needed — purely frontend change
  • Observable failure: Theme toggle missing or broken → visual only, no functional impact

Risks and Mitigations

  • Risk: CSS variables may conflict with shadcn/ui defaults
    • Mitigation: Uses standard CSS custom property pattern, overrides only what is needed
  • Risk: localStorage blocked in some browser contexts
    • Mitigation: Wrapped in try-catch, falls back to system preference then dark

Summary by CodeRabbit

Release Notes

  • New Features
    • Added a theme toggle button to the top navigation for switching between light and dark modes.
    • User theme preference is automatically saved and restored on subsequent visits.

Review Change Stack

fezzzza added 2 commits May 17, 2026 21:58
Adds a theme switcher to the Web UI with light and dark mode support.
Defaults to dark (preserving existing appearance). User preference is
persisted in localStorage and applied before React hydrates to prevent
flash of wrong theme.

Changes:
- index.css: Add :root:not(.dark) light theme variables, rename :root
  to :root.dark for dark theme
- index.html: Add inline script to read localStorage and set .dark
  class before first paint (flash prevention)
- useTheme.ts: New hook that toggles .dark class on <html> and
  persists to localStorage
- TopNav.tsx: Add Sun/Moon toggle button next to version number
Addresses CodeRabbit review comments on coleam00#1713.

- index.html: Wrap pre-mount IIFE in try-catch, fallback to dark mode
- useTheme.ts: Wrap both getItem and setItem in try-catch with comments
  matching the established pattern in ProjectContext/Sidebar/WorkflowBuilder
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 18, 2026

📝 Walkthrough

Walkthrough

The PR introduces a complete light/dark theme system for the web application. A new useTheme hook manages theme state and localStorage persistence. CSS theme variables are reorganized under light/dark selectors. Pre-hydration initialization in HTML applies the saved theme before React renders. A toggle button in the top navigation lets users switch themes.

Changes

Theme Toggle Implementation

Layer / File(s) Summary
Theme hook and CSS setup
packages/web/src/hooks/useTheme.ts, packages/web/src/index.css
useTheme hook manages theme state with localStorage persistence and DOM class synchronization. CSS light/dark theme variables are scoped under :root:not(.dark) and :root.dark respectively, replacing the previous dark-only default.
Pre-hydration theme initialization
packages/web/index.html
HTML adds inline CSS for light/dark body backgrounds and an IIFE script that reads archon-theme from localStorage before React hydrates, applying the dark class to prevent theme flash.
Theme toggle button in top navigation
packages/web/src/components/layout/TopNav.tsx
TopNav imports Sun/Moon icons, calls useTheme, and renders a button that toggles the theme with appropriate icons and accessibility labels.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A toggle button hops into view,
Light and dark, the themes are true,
Pre-hydration script prevents the flash,
localStorage keeps the theme stash!
—ArchonRabbit 🌙✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main feature addition of a light/dark theme toggle, directly matching the core objective and changes across the codebase.
Description check ✅ Passed The description follows the template comprehensively, covering problem statement, impact, detailed change summary, UX journey with before/after diagrams, architecture changes, validation evidence, security analysis, compatibility, and rollback plan.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/web/index.html`:
- Around line 25-39: The pre-hydration script currently forces dark mode when
localStorage access fails or no saved value exists; change the IIFE so after
reading 'archon-theme' it only applies dark when the value is 'dark', removes
dark when 'light', and if the value is missing or storage throws fall back to
the system preference via window.matchMedia('(prefers-color-scheme: dark)').use
the existing document.documentElement.classList.add('dark') and
.classList.remove('dark') calls and the same 'archon-theme' key so the logic in
the anonymous function matches saved preference first, saved 'light'/'dark'
explicitly, otherwise consult matchMedia.

In `@packages/web/src/hooks/useTheme.ts`:
- Around line 7-16: getInitialTheme currently falls back to 'dark' for
first-time users instead of respecting the system preference; update
getInitialTheme to, after failing to read localStorage (or when no saved value),
check window.matchMedia('(prefers-color-scheme: dark)').matches and return
'dark' if true or 'light' if false, while keeping the existing server-side guard
(typeof window === 'undefined') and the try/catch around localStorage access for
Safari/private-mode safety; reference STORAGE_KEY and the getInitialTheme
function when making the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ddc844fd-b3e4-4743-9bc7-c446591b123f

📥 Commits

Reviewing files that changed from the base of the PR and between 7bdf931 and 94b79fe.

📒 Files selected for processing (4)
  • packages/web/index.html
  • packages/web/src/components/layout/TopNav.tsx
  • packages/web/src/hooks/useTheme.ts
  • packages/web/src/index.css

Comment thread packages/web/index.html
Comment on lines +25 to +39
<script>
// Apply saved theme before React renders to prevent flash
(function () {
try {
var theme = localStorage.getItem('archon-theme');
if (theme === 'light') {
document.documentElement.classList.remove('dark');
} else {
document.documentElement.classList.add('dark');
}
} catch (_) {
document.documentElement.classList.add('dark');
}
})();
</script>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Pre-hydration fallback should respect system theme, not always dark.

Lines 32-37 force dark when there is no saved value (or storage throws), so first paint can be wrong for users with light OS preference.

Suggested fix
       (function () {
+        var isDark;
         try {
           var theme = localStorage.getItem('archon-theme');
-          if (theme === 'light') {
-            document.documentElement.classList.remove('dark');
-          } else {
-            document.documentElement.classList.add('dark');
-          }
+          if (theme === 'light') isDark = false;
+          else if (theme === 'dark') isDark = true;
+          else isDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
         } catch (_) {
-          document.documentElement.classList.add('dark');
+          isDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
         }
+        document.documentElement.classList.toggle('dark', !!isDark);
       })();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/index.html` around lines 25 - 39, The pre-hydration script
currently forces dark mode when localStorage access fails or no saved value
exists; change the IIFE so after reading 'archon-theme' it only applies dark
when the value is 'dark', removes dark when 'light', and if the value is missing
or storage throws fall back to the system preference via
window.matchMedia('(prefers-color-scheme: dark)').use the existing
document.documentElement.classList.add('dark') and .classList.remove('dark')
calls and the same 'archon-theme' key so the logic in the anonymous function
matches saved preference first, saved 'light'/'dark' explicitly, otherwise
consult matchMedia.

Comment thread packages/web/src/hooks/useTheme.ts
@Wirasm
Copy link
Copy Markdown
Collaborator

Wirasm commented May 19, 2026

Review Summary

Verdict: minor-fixes-needed

Adds a light/dark theme toggle to the web UI with CSS variable-based theming, a useTheme hook, an anti-flash inline script, and a toggle button in TopNav. The core implementation is solid — the anti-flash pattern, accessibility attributes (aria-label, title, type="button"), and CSS variable coverage are all well done. Two items block a "ready" verdict: the new hook has no tests, and a scrollbar comment is inaccurate.

Blocking issues

  • packages/web/src/hooks/useTheme.ts: The entire hook has no test file. toggleTheme, initial state, localStorage integration, and SSR/unavailable-storage fallbacks all go untested. Create packages/web/src/hooks/useTheme.test.ts covering: getInitialTheme defaults to 'dark', reads stored value, falls back to 'dark' on storage errors and during SSR; toggleTheme switches correctly and calls localStorage.setItem with key 'archon-theme'; document.documentElement.classList is updated; gracefully handles unavailable localStorage.
  • packages/web/index.css:206: Comment reads /* Scrollbar styling for dark theme */ but the ruleset applies to both themes. Change to /* Scrollbar styling */ or remove it.

Suggested fixes

  • packages/web/src/hooks/useTheme.ts: The useEffect runs classList.add('dark') on first render even when the inline script already set the class — a redundant DOM operation. Guard it: only apply the class if it differs from the desired theme.
  • TopNav.tsx: Consider an integration test for the theme toggle button once @testing-library/react is adopted, or add it to a Playwright e2e suite.

Minor / nice-to-have

  • useTheme.ts:18: Use const instead of var for the localStorage read.
  • useTheme.ts: Add a comment to the setItem catch block: // Storage unavailable or quota exceeded — theme persists in memory only.
  • TopNav.tsx:16: The multi-line comment explaining payload truncation is verbose — can be shortened to // limit: 1 only needed for counts (runs payload is discarded).
  • index.css:10, 55: Comments "/* Light theme */" and "/* Dark theme */" restate the selectors — remove or trim per comment policy.

Compliments

  • The anti-flash inline script pattern is correct and well-commented. The // Prevent flash: apply theme before React hydrates comment captures a non-obvious decision that will prevent future regressions.
  • The catch block comments in useTheme.ts (localStorage unavailable (Safari private mode, Firefox privacy settings)) are excellent — they explain the WHY and name specific browser modes. Keep this standard.
  • Accessibility is properly addressed on the toggle button.

Reviewed via maintainer-review-pr workflow (Pi/Minimax). Aspects run: code-review, error-handling, test-coverage, comment-quality.

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.

2 participants