Skip to content

feat: add theme toggle to all settings pages ($200 bounty) - #301

Open
foxyManTou wants to merge 1 commit into
bountydotnew:mainfrom
foxyManTou:foxy/dark-mode-settings
Open

feat: add theme toggle to all settings pages ($200 bounty)#301
foxyManTou wants to merge 1 commit into
bountydotnew:mainfrom
foxyManTou:foxy/dark-mode-settings

Conversation

@foxyManTou

@foxyManTou foxyManTou commented Jul 7, 2026

Copy link
Copy Markdown

Summary

Ships dark mode / theme switching to all settings pages as requested in the bounty.

Changes

  • New ThemeToggle component in settings layouts (org-scoped and personal)

    • Compact pill-button UI: Light / Dark / System
    • Uses next-themes useTheme() — already wired in Providers
    • Matches existing design system tokens (border-subtle, surface-hover, primary)
  • org settings layout (/[slug]/settings/layout.tsx) — theme toggle at top of scroll area

  • personal settings layout (/settings/layout.tsx) — theme toggle at top of scroll area

Coverage

The toggle now appears on every settings sub-page:

  • Account (has existing AppearanceSection with image-preview cards for bonus UX)
  • General
  • Billing
  • Payments
  • Members

Design

Theme  [ ☀️ Light ] [ 🌙 Dark ] [ 💻 System ]

Compact, accessible, uses existing lucide icons, no external deps, no API calls needed (theme is fully client-side).

PR Links

Closes the dark mode bounty.

Summary by CodeRabbit

  • New Features
    • Added theme switching controls for settings pages, letting users choose between light, dark, and system appearance.
    • Introduced a selectable theme panel with clear active-state styling and accessibility support.
    • Updated settings layouts to surface theme controls at the top of the page in a cleaner, responsive container.

Ship dark mode toggle for settings pages (bounty.$200)

- Added ThemeToggle component to org settings layout
- Added ThemeToggle component to personal settings layout
- Toggle available on all settings sub-pages: account, general, billing, payments, members
- Uses next-themes (already wired in providers.tsx)
- Compact pill-button UI: Light / Dark / System
- Consistent design language using existing design tokens (primary, border-subtle, surface-hover)
@vercel

vercel Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Someone is attempting to deploy a commit to the Bounty Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds theme toggle UI controls across two settings layouts ([slug]/settings/layout.tsx and settings/layout.tsx) and introduces a standalone ThemeSelector component, all using next-themes to switch between light, dark, and system themes with active-state styling.

Changes

Theme Toggle Implementation

Layer / File(s) Summary
Organization settings theme toggle
apps/web/src/app/[slug]/settings/layout.tsx
Adds an in-file ThemeToggle client component with light/dark/system buttons and renders it above the existing settings content in the default layout.
Personal settings layout with theme toggle
apps/web/src/app/settings/layout.tsx
Adds a new ThemeToggle component and a default-exported PersonalSettingsLayout that wraps children in a responsive container with the toggle placed in the header area.
Standalone ThemeSelector component
apps/web/src/components/settings/theme-selector.tsx
Adds an exported ThemeSelector component that renders theme options in a Card, reading and updating the active theme via useTheme/setTheme.

Estimated code review effort: 2 (Simple) | ~10 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is mostly complete, but it is missing the required exact "BOUNTY.NEW" notice at the top of the template. Add the exact text "BOUNTY.NEW" as the first line of the description, then keep the existing summary sections.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: adding theme toggle controls to settings pages.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration.


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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 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 `@apps/web/src/app/`[slug]/settings/layout.tsx:
- Around line 4-44: The ThemeToggle logic is duplicated across multiple layouts
and overlaps with ThemeSelector, so extract a single shared theme-picking
component under components/settings and reuse it from the settings layouts. Keep
the theme state handling and button rendering in one place, then update this
ThemeToggle implementation and the matching layout/component to import and
configure that shared component instead of copy-pasting the same UI and logic.
- Around line 4-44: Guard the ThemeToggle render in layout.tsx until the
component is mounted, since useTheme().theme is undefined on the server/first
client pass and currently changes the active class and aria-pressed in
ThemeToggle. Add a mounted state check inside ThemeToggle and return null or
defer the toggle UI until after mount, while keeping the existing theme list,
setTheme handler, and isActive logic unchanged once mounted.

In `@apps/web/src/components/settings/theme-selector.tsx`:
- Around line 14-33: The ThemeSelector currently reads useTheme() immediately
and falls back to theme ?? 'system', which can render the wrong active state
during SSR and then flicker on hydration. Update ThemeSelector to delay any
theme-dependent rendering until after the component mounts by using a
mounted/ready state or a placeholder, and keep the currentTheme logic and themes
map rendering gated behind that check.
🪄 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: d17f69b0-732a-4d4b-a37b-64f89587003d

📥 Commits

Reviewing files that changed from the base of the PR and between 0377c8f and 3db550d.

📒 Files selected for processing (3)
  • apps/web/src/app/[slug]/settings/layout.tsx
  • apps/web/src/app/settings/layout.tsx
  • apps/web/src/components/settings/theme-selector.tsx

Comment on lines +4 to +44
import { Sun, Moon, Monitor } from 'lucide-react';
import { cn } from '@bounty/ui/lib/utils';
import { useTheme } from 'next-themes';

function ThemeToggle() {
const { theme, setTheme } = useTheme();

const themes = [
{ name: 'light', label: 'Light', icon: Sun },
{ name: 'dark', label: 'Dark', icon: Moon },
{ name: 'system', label: 'System', icon: Monitor },
] as const;

return (
<div className="flex flex-col gap-2 mb-4">
<label className="text-xs font-medium text-text-secondary">Theme</label>
<div className="flex gap-1.5">
{themes.map(({ name, label, icon: Icon }) => {
const isActive = theme === name;
return (
<button
key={name}
type="button"
onClick={() => setTheme(name)}
className={cn(
'inline-flex items-center gap-1.5 px-2.5 py-1.5 rounded-md text-xs font-medium border transition-colors',
isActive
? 'bg-primary/10 border-primary/40 text-foreground'
: 'border-border-subtle bg-background text-text-secondary hover:bg-surface-hover hover:text-foreground'
)}
aria-pressed={isActive}
>
<Icon className="h-3 w-3" />
{label}
</button>
);
})}
</div>
</div>
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Duplicate ThemeToggle implementation across layouts.

This component is copy-pasted verbatim in apps/web/src/app/settings/layout.tsx, and a third, visually different ThemeSelector exists in apps/web/src/components/settings/theme-selector.tsx implementing the same "pick a theme" concept. Consider extracting one shared, parameterizable theme-toggle component under components/settings/ and importing it in both layouts instead of duplicating the logic and styling.

Also applies to: 53-55

🤖 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 `@apps/web/src/app/`[slug]/settings/layout.tsx around lines 4 - 44, The
ThemeToggle logic is duplicated across multiple layouts and overlaps with
ThemeSelector, so extract a single shared theme-picking component under
components/settings and reuse it from the settings layouts. Keep the theme state
handling and button rendering in one place, then update this ThemeToggle
implementation and the matching layout/component to import and configure that
shared component instead of copy-pasting the same UI and logic.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== layout.tsx ==\n'
sed -n '1,220p' apps/web/src/app/'[slug]'/settings/layout.tsx

printf '\n== search next-themes usage ==\n'
rg -n "useTheme\(|mounted|setMounted|theme === name|aria-pressed" apps/web/src/app apps/web/src/components apps/web/src -g '!**/node_modules/**'

printf '\n== package.json refs ==\n'
rg -n '"next-themes"|next-themes' package.json apps/web/package.json . -g 'package.json'

Repository: bountydotnew/bounty.new

Length of output: 4842


Guard theme-dependent markup until mount.

theme drives the active button class and aria-pressed on the first render. Add a mounted check, or defer rendering this toggle, so server and client markup stay consistent.

🩹 Proposed fix: guard render until mounted
 function ThemeToggle() {
   const { theme, setTheme } = useTheme();
+  const [mounted, setMounted] = React.useState(false);
+
+  React.useEffect(() => {
+    setMounted(true);
+  }, []);
+
+  if (!mounted) return null;
 
   const themes = [
🤖 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 `@apps/web/src/app/`[slug]/settings/layout.tsx around lines 4 - 44, Guard the
ThemeToggle render in layout.tsx until the component is mounted, since
useTheme().theme is undefined on the server/first client pass and currently
changes the active class and aria-pressed in ThemeToggle. Add a mounted state
check inside ThemeToggle and return null or defer the toggle UI until after
mount, while keeping the existing theme list, setTheme handler, and isActive
logic unchanged once mounted.

Comment on lines +14 to +33
export function ThemeSelector() {
const { theme, setTheme } = useTheme();
const currentTheme = theme ?? 'system';

return (
<Card className="p-5">
<div className="flex items-center justify-between mb-4">
<div>
<h2 className="text-sm font-semibold text-foreground">
Appearance
</h2>
<p className="text-xs text-text-muted mt-0.5">
Choose your preferred theme for the dashboard
</p>
</div>
</div>

<div className="flex flex-wrap gap-2">
{themes.map(({ name, label, icon: Icon }) => {
const isActive = currentTheme === name;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## theme-selector\n'
sed -n '1,220p' apps/web/src/components/settings/theme-selector.tsx

printf '\n## search useTheme and theme provider usage\n'
rg -n "useTheme\\(|ThemeProvider|mounted|system" apps/web/src -g '!**/node_modules/**'

Repository: bountydotnew/bounty.new

Length of output: 7370


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## theme-provider\n'
sed -n '1,220p' apps/web/src/components/theme-provider.tsx

printf '\n## app providers\n'
sed -n '1,220p' apps/web/src/components/providers.tsx

printf '\n## settings layouts using useTheme\n'
sed -n '1,180p' apps/web/src/app/settings/layout.tsx
printf '\n---\n'
sed -n '1,180p' apps/web/src/app/[slug]/settings/layout.tsx

printf '\n## find mounted/suppressHydrationWarning in web app\n'
rg -n "suppressHydrationWarning|mounted|resolvedTheme|ThemeProvider" apps/web/src -g '!**/node_modules/**'

Repository: bountydotnew/bounty.new

Length of output: 8656


🌐 Web query:

next-themes hydration mismatch mounted theme-dependent UI documentation

💡 Result:

Hydration mismatches occur when using next-themes because theme information—stored in browser APIs like localStorage—is unavailable during server-side rendering (SSR) [1][2]. Consequently, the theme values returned by the useTheme hook are undefined on the server but resolve to a specific theme (e.g., 'light' or 'dark') once the component mounts on the client [1][2]. React detects this inconsistency between the initial server-rendered HTML and the client's expected state, triggering a hydration error [1][3]. To address this, there are two primary approaches: 1. The Mounted State Pattern (Classic Solution) To prevent the error, you must delay rendering any theme-dependent UI until the component has mounted on the client [2]. You can implement this by creating a custom hook or using a local state within your component: - Initialize a mounted state to false [1][3]. - Use useEffect to set it to true once the component mounts [1][3]. - Conditionally render your theme-dependent components only after the state is true (or render a placeholder/skeleton while false) [3][2]. Example: const [mounted, setMounted] = useState(false); useEffect( => setMounted(true), []); if (!mounted) return null; // Or return a loading skeleton return

{theme}
; 2. The Data-Attribute/Tailwind Approach (Modern Solution) You can avoid the mounted state entirely by configuring next-themes to use data attributes instead of class names [4][5]. By applying an attribute like data-theme and using CSS or Tailwind variants that react to that attribute, the rendered HTML remains consistent between the server and the client [4][5]. Since the data attribute is injected via an inline script before hydration, the theme-dependent styles are applied correctly without React needing to toggle classes or change the DOM tree [4][5]. Important Considerations: - Root Layout Warning: Always add the suppressHydrationWarning prop to the tag in your root layout [6][7]. This is necessary because next-themes modifies the element attributes (class/style) to apply the theme, which technically deviates from the server-rendered HTML [8][7]. Note that this prop is one level deep and does not suppress warnings for child components [1][7]. - Avoiding Layout Shift: When using the mounted state pattern, it is recommended to render a skeleton or placeholder to avoid content layout shifts (CLS) while waiting for the client to mount [2].

Citations:


Delay rendering this selector until it mounts. theme ?? 'system' still renders “System” as active on the server, then flips to the persisted theme on the client and causes a hydration mismatch/flicker. Gate the theme-dependent UI behind a mounted check or placeholder before reading theme.

🤖 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 `@apps/web/src/components/settings/theme-selector.tsx` around lines 14 - 33,
The ThemeSelector currently reads useTheme() immediately and falls back to theme
?? 'system', which can render the wrong active state during SSR and then flicker
on hydration. Update ThemeSelector to delay any theme-dependent rendering until
after the component mounts by using a mounted/ready state or a placeholder, and
keep the currentTheme logic and themes map rendering gated behind that check.

@foxyManTou

Copy link
Copy Markdown
Author

🦊 Friendly ping! This PR implements the theme toggle for all settings pages as per the $200 bounty. Ready for review and merge. Payment to 0x93bd7eb8d98cb6a6ce0a72010d4536fbe5391241 (Base) via Stripe.

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