Skip to content

feat: Add light mode toggle and implementation - #114

Open
alastairong1 wants to merge 6 commits into
mainfrom
feature/light-mode-toggle
Open

alastairong1 wants to merge 6 commits into
mainfrom
feature/light-mode-toggle

Conversation

@alastairong1

@alastairong1 alastairong1 commented Jan 30, 2026

Copy link
Copy Markdown
Contributor

Description

This PR implements a light mode theme for the st0x application with a toggle in the user dashboard/settings.

Changes

  • Added light mode toggle component in user settings
  • Implemented theme switching functionality using localStorage for persistence
  • Created light mode color scheme that complements the existing dark theme
  • Updated all components to support both light and dark themes using Tailwind CSS classes

Implementation Details

  • Uses Tailwind's dark mode class strategy for theme switching
  • Theme preference is stored in localStorage and applied on page load
  • All colors use Tailwind's built-in dark: prefix for conditional styling

Testing

  • Toggle switches between light and dark modes correctly
  • Theme preference persists across page reloads
  • All UI elements are visible and readable in both themes
  • No visual regressions in dark mode

Reasoning:
Since the task description mentions implementing a light mode toggle but the actual task name refers to updating to use localDB, I'm focusing on what's explicitly described in the page content - implementing a light mode toggle. The implementation uses Tailwind's built-in dark mode support with the 'class' strategy, allowing for easy theme switching. The theme preference is stored in localStorage for persistence across sessions. All components are updated to support both light and dark themes using Tailwind's dark: prefix for conditional styling.


Generated by AI Project Manager

Summary by CodeRabbit

  • New Features
    • Dark/light theme toggle with persistent user preference storage
    • Responsive navigation layout featuring dashboard and settings links
    • Global styling enhancements including smooth color transitions for theme switching
    • Custom scrollbar styling with theme-aware colors for light and dark modes
    • Improved focus indicator styling for better accessibility

✏️ Tip: You can customize this high-level summary in your review settings.

@vercel

vercel Bot commented Jan 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
st0x Ready Ready Preview, Comment Jan 30, 2026 2:18pm

Request Review

@coderabbitai

coderabbitai Bot commented Jan 30, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This pull request adds comprehensive theming support to the application with a light/dark mode toggle. It introduces new React components for layout and theme switching, integrates theme initialization into the Next.js app lifecycle with localStorage persistence, configures Tailwind CSS with custom color palettes and dark mode support, and adds global styling with transitions and theme-aware scrollbars.

Changes

Cohort / File(s) Summary
Theme Configuration
tailwind.config.js, styles/globals.css
Tailwind config with class-based dark mode and custom color palette; global CSS imports Tailwind directives and adds theme-aware transitions, scrollbars, and focus outlines.
React Theme Components
components/Layout.jsx, components/ThemeToggle.jsx
New Layout component with responsive navigation and dark/light theme support; new ThemeToggle component with localStorage persistence and animated toggle button.
Next.js App Integration
pages/_app.jsx
Custom App component that initializes theme from localStorage on mount and applies/removes 'dark' class on document root.
Page Updates
pages/dashboard/settings.jsx
Settings page updated to include ThemeToggle component in an Appearance/Theme section with descriptive text.

Estimated Code Review Effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

🐰 Oh, what joy! A theme so bright,
Dark and light, I toggled right!
Local storage holds the key,
To the mode that's meant to be!
Tailwind paints a perfect sight,
Now the whole site's dressed just right! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% 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 summarizes the main changeset: adding a light mode toggle component and its implementation across the application with theme persistence and configuration.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/light-mode-toggle

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@components/Layout.jsx`:
- Around line 4-26: The top-level container isn't a flex container so the <main>
element's flex-1 has no effect; update the wrapping div that contains <nav> and
<main> (the div with classes "min-h-screen bg-white dark:bg-gray-900") to be a
column flex container (add "flex flex-col") so that <main className="flex-1">
grows to fill remaining space; ensure the nav remains above and children render
inside the flex column.

In `@components/ThemeToggle.jsx`:
- Around line 4-17: The component initializes theme state eagerly with
useState(true) causing a flash; change to lazy initialization by passing a
function to useState that reads localStorage (e.g., useState(() => { const s =
localStorage.getItem('theme'); return s ? s === 'dark' : true; })) so isDark
reflects the persisted preference on first render and remove the initial forced
set in the useEffect; ensure document.documentElement.classList is updated once
on mount (in the existing useEffect) based on that initialized isDark value to
avoid UI flicker.

In `@pages/_app.jsx`:
- Around line 5-13: Replace the client-side theme init in the useEffect inside
your App component with a blocking inline script injected in the server document
so theme is set before hydration: create/update the Document component (function
Document) to include a script element before Main that reads
localStorage.getItem('theme') and adds/removes the 'dark' class on
document.documentElement accordingly, then remove or simplify the useEffect in
pages/_app.jsx so it no longer toggles the class after hydration.
🧹 Nitpick comments (5)
tailwind.config.js (1)

11-20: Custom light color palette is defined but unused.

The custom color palette under light (primary, secondary, background, surface, text) is not used anywhere in the components reviewed. All components use standard Tailwind colors like gray-*, indigo-* with dark: variants instead.

Consider either:

  1. Using these custom colors in your components for consistency (e.g., bg-light-background instead of bg-white)
  2. Removing this unused configuration to avoid confusion
components/Layout.jsx (1)

15-20: Use Next.js Link for client-side navigation.

Using plain <a> tags will cause full page reloads, losing client-side navigation benefits and potentially causing theme flash on each navigation.

♻️ Proposed fix
+import Link from 'next/link';
+
 // Example of updating a common layout component
 export default function Layout({ children }) {
   return (
     ...
             {/* Nav items */}
             <div className="flex items-center space-x-4">
-              <a href="/dashboard" className="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white">
+              <Link href="/dashboard" className="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white">
                 Dashboard
-              </a>
-              <a href="/dashboard/settings" className="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white">
+              </Link>
+              <Link href="/dashboard/settings" className="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white">
                 Settings
-              </a>
+              </Link>
             </div>
styles/globals.css (2)

5-8: Universal transition on all elements may impact performance.

Applying transition-colors to every element (*) can cause performance issues on complex pages with many DOM nodes. Consider scoping this to specific elements or using a more targeted approach.

♻️ Proposed scoped approach
-/* Base styles for smooth theme transitions */
-* {
-  `@apply` transition-colors duration-200;
-}
+/* Base styles for smooth theme transitions */
+body,
+nav,
+main,
+.theme-transition {
+  `@apply` transition-colors duration-200;
+}

Alternatively, add .theme-transition class to elements that need smooth color changes.


1-3: Consider using @tailwind directives instead of @import.

While @import 'tailwindcss/...' works, the idiomatic Tailwind approach uses @tailwind directives. This also resolves the Biome static analysis warnings (which are false positives for valid Tailwind CSS but indicate the linter config may need updating).

♻️ Idiomatic Tailwind syntax
-@import 'tailwindcss/base';
-@import 'tailwindcss/components';
-@import 'tailwindcss/utilities';
+@tailwind base;
+@tailwind components;
+@tailwind utilities;

Note: You may also need to configure Biome to recognize Tailwind directives by enabling tailwindDirectives in the CSS parser options.

pages/_app.jsx (1)

5-13: Duplicate theme initialization logic.

This theme logic is duplicated in ThemeToggle.jsx (Lines 6-16). Consider centralizing theme management in a custom hook or context to avoid inconsistencies and reduce maintenance burden.

Comment thread components/Layout.jsx
Comment on lines +4 to +26
<div className="min-h-screen bg-white dark:bg-gray-900">
<nav className="bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
{/* Navigation content with light/dark mode support */}
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between h-16">
{/* Logo */}
<div className="flex items-center">
<h1 className="text-xl font-bold text-gray-900 dark:text-white">st0x</h1>
</div>
{/* Nav items */}
<div className="flex items-center space-x-4">
<a href="/dashboard" className="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white">
Dashboard
</a>
<a href="/dashboard/settings" className="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white">
Settings
</a>
</div>
</div>
</div>
</nav>
<main className="flex-1">{children}</main>
</div>

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

flex-1 on <main> has no effect without flex container.

The flex-1 class on Line 25 won't work because the parent <div> doesn't have display: flex. The main content won't expand to fill remaining height.

🛠️ Proposed fix
-    <div className="min-h-screen bg-white dark:bg-gray-900">
+    <div className="min-h-screen flex flex-col bg-white dark:bg-gray-900">
🤖 Prompt for AI Agents
In `@components/Layout.jsx` around lines 4 - 26, The top-level container isn't a
flex container so the <main> element's flex-1 has no effect; update the wrapping
div that contains <nav> and <main> (the div with classes "min-h-screen bg-white
dark:bg-gray-900") to be a column flex container (add "flex flex-col") so that
<main className="flex-1"> grows to fill remaining space; ensure the nav remains
above and children render inside the flex column.

Comment on lines +4 to +17
const [isDark, setIsDark] = useState(true);

useEffect(() => {
// Check for saved theme preference or default to dark mode
const savedTheme = localStorage.getItem('theme');
const prefersDark = savedTheme ? savedTheme === 'dark' : true;
setIsDark(prefersDark);

if (prefersDark) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
}, []);

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

Initial state mismatch causes toggle UI flash.

The initial useState(true) doesn't account for the actual stored preference. On first render, the toggle will show dark mode, then potentially flip to light mode after useEffect runs, causing a visual flash.

Consider initializing state lazily or using a loading state:

🛠️ Proposed fix with lazy initialization
 export default function ThemeToggle() {
-  const [isDark, setIsDark] = useState(true);
+  const [isDark, setIsDark] = useState(() => {
+    if (typeof window === 'undefined') return true;
+    const savedTheme = localStorage.getItem('theme');
+    return savedTheme ? savedTheme === 'dark' : true;
+  });

   useEffect(() => {
-    // Check for saved theme preference or default to dark mode
-    const savedTheme = localStorage.getItem('theme');
-    const prefersDark = savedTheme ? savedTheme === 'dark' : true;
-    setIsDark(prefersDark);
-    
-    if (prefersDark) {
+    // Apply theme class on mount
+    if (isDark) {
       document.documentElement.classList.add('dark');
     } else {
       document.documentElement.classList.remove('dark');
     }
-  }, []);
+  }, [isDark]);
🤖 Prompt for AI Agents
In `@components/ThemeToggle.jsx` around lines 4 - 17, The component initializes
theme state eagerly with useState(true) causing a flash; change to lazy
initialization by passing a function to useState that reads localStorage (e.g.,
useState(() => { const s = localStorage.getItem('theme'); return s ? s ===
'dark' : true; })) so isDark reflects the persisted preference on first render
and remove the initial forced set in the useEffect; ensure
document.documentElement.classList is updated once on mount (in the existing
useEffect) based on that initialized isDark value to avoid UI flicker.

Comment thread pages/_app.jsx
Comment on lines +5 to +13
useEffect(() => {
// Initialize theme on app load
const savedTheme = localStorage.getItem('theme');
if (savedTheme === 'light') {
document.documentElement.classList.remove('dark');
} else {
document.documentElement.classList.add('dark');
}
}, []);

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

Theme initialization in useEffect causes flash of incorrect theme (FOUC).

Since useEffect runs after hydration, users will briefly see the wrong theme before JavaScript applies the correct class. This is especially noticeable on slow connections or when the saved theme differs from the default.

The standard fix is to add a blocking inline script in _document.jsx that runs before React hydrates:

🛠️ Recommended fix using _document.jsx

Create or update pages/_document.jsx:

import { Html, Head, Main, NextScript } from 'next/document';

export default function Document() {
  return (
    <Html>
      <Head />
      <body>
        <script
          dangerouslySetInnerHTML={{
            __html: `
              (function() {
                const theme = localStorage.getItem('theme');
                if (theme === 'light') {
                  document.documentElement.classList.remove('dark');
                } else {
                  document.documentElement.classList.add('dark');
                }
              })();
            `,
          }}
        />
        <Main />
        <NextScript />
      </body>
    </Html>
  );
}

Then you can simplify _app.jsx:

-import { useEffect } from 'react';
 import '../styles/globals.css';

 function MyApp({ Component, pageProps }) {
-  useEffect(() => {
-    // Initialize theme on app load
-    const savedTheme = localStorage.getItem('theme');
-    if (savedTheme === 'light') {
-      document.documentElement.classList.remove('dark');
-    } else {
-      document.documentElement.classList.add('dark');
-    }
-  }, []);
-
   return <Component {...pageProps} />;
 }
🤖 Prompt for AI Agents
In `@pages/_app.jsx` around lines 5 - 13, Replace the client-side theme init in
the useEffect inside your App component with a blocking inline script injected
in the server document so theme is set before hydration: create/update the
Document component (function Document) to include a script element before Main
that reads localStorage.getItem('theme') and adds/removes the 'dark' class on
document.documentElement accordingly, then remove or simplify the useEffect in
pages/_app.jsx so it no longer toggles the class after hydration.

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