Skip to content

feat(ui): add dark mode toggle and stabilize docs sidebar on mobile#1

Open
0xEgao wants to merge 1 commit intocitadel-tech:mainfrom
0xEgao:dark
Open

feat(ui): add dark mode toggle and stabilize docs sidebar on mobile#1
0xEgao wants to merge 1 commit intocitadel-tech:mainfrom
0xEgao:dark

Conversation

@0xEgao
Copy link
Copy Markdown

@0xEgao 0xEgao commented Apr 4, 2026

Summary by CodeRabbit

  • New Features

    • Added dark/light theme toggle in the header with persistent theme preference.
    • App now supports dark mode with comprehensive themed colors throughout.
  • Improvements

    • Sidebar prevents background scrolling when open on mobile.
    • Sidebar automatically closes when navigating to a different page.
    • Enhanced accessibility with improved aria labels and controls.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 4, 2026

📝 Walkthrough

Walkthrough

This PR introduces a dark theme feature with localStorage persistence. The Header component now includes theme toggle functionality with UI updates for both desktop and mobile layouts. Comprehensive dark mode CSS variables and styling are added to override colors and component-specific styles. The Docs component is updated with improved sidebar scroll behavior and accessibility enhancements.

Changes

Cohort / File(s) Summary
Theme Feature Implementation
src/components/layout/Header.jsx, src/index.css
Introduces theme persistence via localStorage with state management in Header component. Adds ThemeToggle UI element and restructures layout for desktop/mobile. CSS defines dark mode variables, color overrides for utility classes, typography adjustments, and component-specific dark theme styling.
Docs Sidebar Improvements
src/pages/Docs.jsx
Adds overflow prevention when sidebar is open, resets sidebar state on active doc changes, updates class names from inline Tailwind values to semantic classes, enhances accessibility with aria attributes, and adjusts spacing values.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🌙 ✨ A toggle appears, a moon and a sun,
Dark mode now works for everyone!
Local storage keeps the choice so tight,
Hopping through themes, from day into night.
The CSS dances in colors so grand,
A theme-switching feast across all the land! 🐰

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main changes: adding a dark mode toggle feature and improving docs sidebar mobile behavior, both of which are evident in the changeset.

✏️ 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: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/layout/Header.jsx`:
- Around line 15-22: The page can show a wrong theme briefly because
resolveInitialTheme runs only during React hydration; add a small blocking
inline script in your HTML <head> that reads the same storage key
(THEME_STORAGE_KEY) and applies it to document.documentElement (set a data-theme
attribute and/or toggle the "dark" class) before the React bundle loads, falling
back to matchMedia('(prefers-color-scheme: dark)') if the stored value is absent
or invalid; keep resolveInitialTheme as-is so React reads the same source of
truth on hydration.
- Around line 74-78: The effect in useEffect currently always writes the
resolved theme to localStorage (THEME_STORAGE_KEY) on mount/changes which
overrides system-preference users; modify the logic to write to localStorage
only when the user has explicitly chosen a theme: add a piece of state/prop like
hasUserChosenTheme (or isUserThemePreference) and update that flag when the user
uses the theme toggle handler, then change the useEffect that calls
document.documentElement.setAttribute and classList.toggle to always set DOM
theme but call window.localStorage.setItem(THEME_STORAGE_KEY, theme) only if
hasUserChosenTheme is true so initial mount respects system preference.

In `@src/index.css`:
- Around line 119-148: The dark-mode CSS block repeats many !important overrides
for selectors like html[data-theme='dark'] .text-black and
.border-black\/35/.bg-white\/30; replace these per-utility overrides by defining
a semantic RGB CSS variable (e.g. --text-primary and --border-primary) inside
html[data-theme='dark'] and :root, then remove the repeated !important rules and
update components to use Tailwind arbitrary colors that reference the variables
(e.g. text-[rgb(var(--text-primary)/0.8)] or
border-[rgb(var(--border-primary)/0.35)]), allowing Tailwind opacity modifiers
to work without specificity hacks.

In `@src/pages/Docs.jsx`:
- Around line 21-28: Docs.jsx and Header.jsx both directly mutate
document.body.style.overflow causing cleanup race conditions; extract and use a
shared hook (e.g., create useScrollLock) that accepts a boolean (use it with
Docs' sidebarOpen and Header's open) to centrally increment/decrement a lock
count, set overflow='hidden' when count>0 and only restore overflow when count
reaches 0, then replace the existing direct body.style.overflow effects in both
components with calls to useScrollLock(sidebarOpen) and useScrollLock(open).
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 21b71928-49a1-4994-bb15-87a7467091b2

📥 Commits

Reviewing files that changed from the base of the PR and between ea1b08d and 317dd23.

📒 Files selected for processing (3)
  • src/components/layout/Header.jsx
  • src/index.css
  • src/pages/Docs.jsx

Comment on lines +15 to +22
function resolveInitialTheme() {
if (typeof window === 'undefined') return 'light'

const stored = window.localStorage.getItem(THEME_STORAGE_KEY)
if (stored === 'light' || stored === 'dark') return stored

return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Potential flash of unstyled content (FOUC) on page load.

The theme is resolved in React state after hydration, which means users may briefly see the wrong theme before React runs. For a smoother experience, consider adding a blocking script in the HTML <head> that sets data-theme before the page renders.

💡 Inline script to prevent theme flash

Add this script to your index.html before the React bundle loads:

<script>
  (function() {
    var stored = localStorage.getItem('coinswap-theme');
    var theme = stored === 'light' || stored === 'dark' 
      ? stored 
      : (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
    document.documentElement.setAttribute('data-theme', theme);
    document.documentElement.classList.toggle('dark', theme === 'dark');
  })();
</script>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/layout/Header.jsx` around lines 15 - 22, The page can show a
wrong theme briefly because resolveInitialTheme runs only during React
hydration; add a small blocking inline script in your HTML <head> that reads the
same storage key (THEME_STORAGE_KEY) and applies it to document.documentElement
(set a data-theme attribute and/or toggle the "dark" class) before the React
bundle loads, falling back to matchMedia('(prefers-color-scheme: dark)') if the
stored value is absent or invalid; keep resolveInitialTheme as-is so React reads
the same source of truth on hydration.

Comment on lines +74 to +78
useEffect(() => {
document.documentElement.setAttribute('data-theme', theme)
document.documentElement.classList.toggle('dark', theme === 'dark')
window.localStorage.setItem(THEME_STORAGE_KEY, theme)
}, [theme])
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 | 🟡 Minor

Theme effect writes to localStorage on every theme change, including initial mount.

On initial render, this writes the resolved theme to localStorage even if the user never explicitly chose a preference. This may override the system preference behavior for users who expect automatic theme switching based on OS settings.

🛡️ Proposed fix to preserve system preference

Track whether the user has explicitly chosen a theme:

+const THEME_STORAGE_KEY = 'coinswap-theme'
+
+function resolveInitialTheme() {
+  if (typeof window === 'undefined') return 'light'
+
+  const stored = window.localStorage.getItem(THEME_STORAGE_KEY)
+  if (stored === 'light' || stored === 'dark') return stored
+
+  return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
+}
+
+function hasStoredPreference() {
+  if (typeof window === 'undefined') return false
+  const stored = window.localStorage.getItem(THEME_STORAGE_KEY)
+  return stored === 'light' || stored === 'dark'
+}

// In component:
-  useEffect(() => {
-    document.documentElement.setAttribute('data-theme', theme)
-    document.documentElement.classList.toggle('dark', theme === 'dark')
-    window.localStorage.setItem(THEME_STORAGE_KEY, theme)
-  }, [theme])
+  const [hasExplicitChoice, setHasExplicitChoice] = useState(hasStoredPreference)
+
+  useEffect(() => {
+    document.documentElement.setAttribute('data-theme', theme)
+    document.documentElement.classList.toggle('dark', theme === 'dark')
+    if (hasExplicitChoice) {
+      window.localStorage.setItem(THEME_STORAGE_KEY, theme)
+    }
+  }, [theme, hasExplicitChoice])
+
+  function toggleTheme() {
+    setHasExplicitChoice(true)
+    setTheme(current => (current === 'dark' ? 'light' : 'dark'))
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/layout/Header.jsx` around lines 74 - 78, The effect in
useEffect currently always writes the resolved theme to localStorage
(THEME_STORAGE_KEY) on mount/changes which overrides system-preference users;
modify the logic to write to localStorage only when the user has explicitly
chosen a theme: add a piece of state/prop like hasUserChosenTheme (or
isUserThemePreference) and update that flag when the user uses the theme toggle
handler, then change the useEffect that calls
document.documentElement.setAttribute and classList.toggle to always set DOM
theme but call window.localStorage.setItem(THEME_STORAGE_KEY, theme) only if
hasUserChosenTheme is true so initial mount respects system preference.

Comment thread src/index.css
Comment on lines +119 to +148
html[data-theme='dark'] .text-black { color: #edf3ff !important; }
html[data-theme='dark'] .text-black\/80 { color: rgb(237 243 255 / 0.8) !important; }
html[data-theme='dark'] .text-black\/78 { color: rgb(237 243 255 / 0.78) !important; }
html[data-theme='dark'] .text-black\/70 { color: rgb(237 243 255 / 0.7) !important; }
html[data-theme='dark'] .text-black\/65 { color: rgb(237 243 255 / 0.65) !important; }
html[data-theme='dark'] .text-black\/60 { color: rgb(237 243 255 / 0.6) !important; }
html[data-theme='dark'] .text-black\/55 { color: rgb(237 243 255 / 0.55) !important; }
html[data-theme='dark'] .text-black\/52 { color: rgb(237 243 255 / 0.52) !important; }
html[data-theme='dark'] .text-black\/50 { color: rgb(237 243 255 / 0.5) !important; }
html[data-theme='dark'] .text-black\/45 { color: rgb(237 243 255 / 0.45) !important; }
html[data-theme='dark'] .text-black\/42 { color: rgb(237 243 255 / 0.42) !important; }
html[data-theme='dark'] .text-black\/35 { color: rgb(237 243 255 / 0.35) !important; }
html[data-theme='dark'] .text-black\/30 { color: rgb(237 243 255 / 0.3) !important; }
html[data-theme='dark'] .text-black\/28 { color: rgb(237 243 255 / 0.28) !important; }

html[data-theme='dark'] .border-black\/35 { border-color: rgb(237 243 255 / 0.35) !important; }
html[data-theme='dark'] .border-black\/25 { border-color: rgb(237 243 255 / 0.25) !important; }
html[data-theme='dark'] .border-black\/20 { border-color: rgb(237 243 255 / 0.2) !important; }
html[data-theme='dark'] .border-black\/15 { border-color: rgb(237 243 255 / 0.15) !important; }
html[data-theme='dark'] .border-black\/12 { border-color: rgb(237 243 255 / 0.12) !important; }
html[data-theme='dark'] .border-black\/10 { border-color: rgb(237 243 255 / 0.1) !important; }
html[data-theme='dark'] .border-black\/8 { border-color: rgb(237 243 255 / 0.08) !important; }

html[data-theme='dark'] .bg-white\/30 { background-color: rgb(255 255 255 / 0.08) !important; }
html[data-theme='dark'] .bg-white\/25 { background-color: rgb(255 255 255 / 0.07) !important; }
html[data-theme='dark'] .bg-white\/20 { background-color: rgb(255 255 255 / 0.06) !important; }
html[data-theme='dark'] .bg-white\/15 { background-color: rgb(255 255 255 / 0.05) !important; }
html[data-theme='dark'] .bg-white\/12 { background-color: rgb(255 255 255 / 0.05) !important; }
html[data-theme='dark'] .bg-black\/8 { background-color: rgb(255 255 255 / 0.08) !important; }
html[data-theme='dark'] .bg-black\/4 { background-color: rgb(255 255 255 / 0.05) !important; }
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider reducing !important overrides with a more maintainable approach.

The repeated !important declarations for each opacity variant create maintenance overhead and specificity conflicts. Consider using CSS custom properties for the base color that can be switched in dark mode, allowing Tailwind's opacity modifiers to work naturally.

♻️ Alternative approach using CSS custom properties

Instead of overriding each utility individually, define a semantic color variable:

:root {
  --text-primary: 23 23 23; /* RGB values for light mode */
}

html[data-theme='dark'] {
  --text-primary: 237 243 255; /* RGB values for dark mode */
}

Then use Tailwind's arbitrary value syntax in components: text-[rgb(var(--text-primary)/0.8)]

This eliminates the need for dozens of !important overrides.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/index.css` around lines 119 - 148, The dark-mode CSS block repeats many
!important overrides for selectors like html[data-theme='dark'] .text-black and
.border-black\/35/.bg-white\/30; replace these per-utility overrides by defining
a semantic RGB CSS variable (e.g. --text-primary and --border-primary) inside
html[data-theme='dark'] and :root, then remove the repeated !important rules and
update components to use Tailwind arbitrary colors that reference the variables
(e.g. text-[rgb(var(--text-primary)/0.8)] or
border-[rgb(var(--border-primary)/0.35)]), allowing Tailwind opacity modifiers
to work without specificity hacks.

Comment thread src/pages/Docs.jsx
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.

1 participant