Skip to content

Fix ModeToggle rotation/restart and in-place centering (fixes #73)#74

Merged
DengreSarthak merged 1 commit into
StabilityNexus:mainfrom
ankitkr104:fix/mode-toggle-rotation-73
Apr 3, 2026
Merged

Fix ModeToggle rotation/restart and in-place centering (fixes #73)#74
DengreSarthak merged 1 commit into
StabilityNexus:mainfrom
ankitkr104:fix/mode-toggle-rotation-73

Conversation

@ankitkr104

@ankitkr104 ankitkr104 commented Apr 1, 2026

Copy link
Copy Markdown
Contributor

Summary

  • keep the theme toggle rotation centered in place instead of shifting during the spin
  • make the rotation animation restart cleanly when the toggle is clicked again before the previous animation completes
  • respect reduced-motion preferences for the slow rotation and icon transitions
Recording.2026-04-01.162301.mp4

Testing

  • verified the toggle stays visually centered while switching themes
  • verified repeated rapid clicks restart the animation instead of leaving it in a stale state

@coderabbitai

coderabbitai Bot commented Apr 1, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

CSS animations for theme-toggle functionality added to globals.css, paired with a refactored ModeToggle component that manages animation state via refs and timeouts to control rotation and opacity transitions while respecting reduced motion preferences.

Changes

Cohort / File(s) Summary
Theme Toggle Animations
app/globals.css
Added @keyframes rotate360 for 360° rotation, .rotate-anim utility class applying 1.8s eased rotation with inline-block display, .mode-toggle-icon class for icon transitions, and media query to disable animations for prefers-reduced-motion: reduce.
ModeToggle Component
components/mode-toggle.tsx
Introduced isSwitching state and timeoutRef to manage animation lifecycle; handleToggle resets state on rapid toggles via 20ms timeout and schedules 1800ms timeout to end animation; refactored JSX to apply .rotate-anim to wrapper div and .mode-toggle-icon to individual icons with visibility tied to isDark state.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Suggested labels

Typescript Lang

Poem

🐰 A spin, a twirl, a flash of light,
The moon and sun now dance just right,
With respect for those who rest their eyes,
Animations pause—a courteous prize!
Theme-toggling magic, smooth and neat,
Our CSS dreams are now complete! ✨

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title directly addresses the main changes: fixing ModeToggle rotation/restart behavior and centering, which aligns with the core modifications in both CSS animations and the React component state management.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ 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.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
components/mode-toggle.tsx (1)

67-70: ⚠️ Potential issue | 🟡 Minor

Hardcoded strings should be externalized for internationalization.

The aria-label and screen-reader text are user-visible and should be externalized to resource files per project coding guidelines.

As per coding guidelines: "User-visible strings should be externalized to resource files (i18n)".

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

In `@components/mode-toggle.tsx` around lines 67 - 70, Externalize the
user-visible strings used in the ModeToggle JSX: replace the hardcoded
aria-label value (`aria-label={`Toggle color scheme (current: ${resolvedTheme ??
"light"})`}`) and the screen-reader text inside the <span
className="sr-only">Toggle theme</span> with lookups to your i18n resource
(e.g., use the project's translation function such as useTranslation/t or a
localized strings module), passing resolvedTheme (falling back to the localized
"light" key) into the interpolated aria-label; update the component to import
and use the translation key names (e.g., "modeToggle.ariaLabel" and
"modeToggle.label") instead of hardcoded English strings.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@components/mode-toggle.tsx`:
- Around line 44-47: The 20ms window.setTimeout call that calls
setIsSwitching(true) can fire after the component unmounts; store the timeout id
in a ref (e.g., switchingTimeoutRef) when you call window.setTimeout inside the
component (where setIsSwitching is used) and clear it in a useEffect cleanup (or
the componentWillUnmount equivalent) using
clearTimeout(switchingTimeoutRef.current) to avoid calling setIsSwitching on an
unmounted component; ensure you also reset the ref after clearing so repeated
toggles are handled correctly.
- Around line 15-18: The component sets a timeout via timeoutRef but lacks a
cleanup on unmount, so the pending timeout may call setIsSwitching(false) after
the component unmounts; add a useEffect cleanup that checks timeoutRef.current
and calls clearTimeout(timeoutRef.current) (and sets timeoutRef.current = null)
to prevent the callback from running on an unmounted component; reference the
existing timeoutRef, isSwitching/setIsSwitching and the useEffect hook to add
the cleanup return function.

---

Outside diff comments:
In `@components/mode-toggle.tsx`:
- Around line 67-70: Externalize the user-visible strings used in the ModeToggle
JSX: replace the hardcoded aria-label value (`aria-label={`Toggle color scheme
(current: ${resolvedTheme ?? "light"})`}`) and the screen-reader text inside the
<span className="sr-only">Toggle theme</span> with lookups to your i18n resource
(e.g., use the project's translation function such as useTranslation/t or a
localized strings module), passing resolvedTheme (falling back to the localized
"light" key) into the interpolated aria-label; update the component to import
and use the translation key names (e.g., "modeToggle.ariaLabel" and
"modeToggle.label") instead of hardcoded English strings.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: c08ee497-44e9-48d5-b72d-65acceac3ee1

📥 Commits

Reviewing files that changed from the base of the PR and between d81513c and 6db3fce.

📒 Files selected for processing (2)
  • app/globals.css
  • components/mode-toggle.tsx

Comment on lines +15 to 18
const [isSwitching, setIsSwitching] = useState(false);
const timeoutRef = useRef<number | null>(null);

useEffect(() => setMounted(true), []);

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.

⚠️ Potential issue | 🟠 Major

Missing cleanup effect for timeout — potential memory leak.

If the component unmounts while the animation is running, the timeout callback will attempt to call setIsSwitching(false) on an unmounted component. Add a cleanup effect to clear the timeout.

🛠️ Proposed fix: Add cleanup effect
 const timeoutRef = useRef<number | null>(null);

- useEffect(() => setMounted(true), []);
+ useEffect(() => {
+   setMounted(true);
+   return () => {
+     if (timeoutRef.current) {
+       window.clearTimeout(timeoutRef.current);
+     }
+   };
+ }, []);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const [isSwitching, setIsSwitching] = useState(false);
const timeoutRef = useRef<number | null>(null);
useEffect(() => setMounted(true), []);
const [isSwitching, setIsSwitching] = useState(false);
const timeoutRef = useRef<number | null>(null);
useEffect(() => {
setMounted(true);
return () => {
if (timeoutRef.current) {
window.clearTimeout(timeoutRef.current);
}
};
}, []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/mode-toggle.tsx` around lines 15 - 18, The component sets a
timeout via timeoutRef but lacks a cleanup on unmount, so the pending timeout
may call setIsSwitching(false) after the component unmounts; add a useEffect
cleanup that checks timeoutRef.current and calls
clearTimeout(timeoutRef.current) (and sets timeoutRef.current = null) to prevent
the callback from running on an unmounted component; reference the existing
timeoutRef, isSwitching/setIsSwitching and the useEffect hook to add the cleanup
return function.

Comment on lines +44 to +47
// small tick to allow CSS animation to restart
window.setTimeout(() => {
setIsSwitching(true);
}, 20);

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.

⚠️ Potential issue | 🟡 Minor

Untracked 20ms timeout could cause setState on unmounted component.

The restart timeout isn't stored in a ref, so it can't be cancelled on unmount. While the window is brief, consider tracking it for completeness.

🛠️ Proposed fix
-      // small tick to allow CSS animation to restart
-      window.setTimeout(() => {
-        setIsSwitching(true);
-      }, 20);
+      // small tick to allow CSS animation to restart
+      timeoutRef.current = window.setTimeout(() => {
+        setIsSwitching(true);
+        // Then schedule the end-of-animation timeout
+        timeoutRef.current = window.setTimeout(() => {
+          setIsSwitching(false);
+          timeoutRef.current = null;
+        }, 1800);
+      }, 20);
+      return; // Skip the timeout scheduling below

Alternatively, use a single ref or restructure the logic to chain timeouts properly.

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

In `@components/mode-toggle.tsx` around lines 44 - 47, The 20ms window.setTimeout
call that calls setIsSwitching(true) can fire after the component unmounts;
store the timeout id in a ref (e.g., switchingTimeoutRef) when you call
window.setTimeout inside the component (where setIsSwitching is used) and clear
it in a useEffect cleanup (or the componentWillUnmount equivalent) using
clearTimeout(switchingTimeoutRef.current) to avoid calling setIsSwitching on an
unmounted component; ensure you also reset the ref after clearing so repeated
toggles are handled correctly.

export function ModeToggle() {
const { setTheme, resolvedTheme } = useTheme();
const [mounted, setMounted] = useState(false);
const [isSwitching, setIsSwitching] = useState(false);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can you also improVe the dashboard button bg-colors during theme change? interchange the bg colors between themes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

ok check it also

@DengreSarthak DengreSarthak merged commit 36af825 into StabilityNexus:main Apr 3, 2026
2 checks passed
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