Skip to content

feat: dark/light mode toggle + scrollbar white-space fix (Issue #1639)#1697

Open
AKSHEXXXX wants to merge 1 commit intoreactplay:mainfrom
AKSHEXXXX:feat/dark-light-mode-toggle-1639
Open

feat: dark/light mode toggle + scrollbar white-space fix (Issue #1639)#1697
AKSHEXXXX wants to merge 1 commit intoreactplay:mainfrom
AKSHEXXXX:feat/dark-light-mode-toggle-1639

Conversation

@AKSHEXXXX
Copy link

Summary

Closes #1639

Implements dark/light mode toggling and fixes the scrollbar white-space issue.


Changes

New: ThemeModeProvider context (src/common/theme/theme-context.jsx)

  • Manages theme state ('dark' | 'light')
  • Persists to localStorage under key reactplay.theme
  • Defaults to OS prefers-color-scheme when no stored value
  • Listens for system theme changes while no explicit preference is set

Anti-FOUC inline script (public/index.html)

  • Inline <script> before <div id="root"> reads localStorage / OS preference and sets data-theme on <html> before React renders — prevents flash of wrong theme on load

CSS variable dark theme overrides (src/App.css)

  • html[data-theme='dark'] block inverts the full --color-neutral-* scale (10↔100)
  • All RGB companion variables updated for rgba() usage
  • Brand primary slightly adjusted for dark backgrounds
  • transition: background-color 0.3s ease, color 0.3s ease on body for smooth switching

Scrollbar fix (src/App.css)

  • ::-webkit-scrollbar-track set to transparent (was white, causing white-space gap)
  • Firefox scrollbar-color also updated

Sun/Moon toggle button (src/common/header/HeaderNav.jsx)

  • BsSunFill shown in dark mode, BsMoonStarsFill in light mode
  • aria-label, title, data-testid="theme-toggle-btn" for accessibility

Hero full-width fix (src/common/home/home.css)

  • width: 100vw; margin-left: calc(-50vw + 50%) eliminates side gaps
  • Light and dark mode gradient overrides added

Hardcoded hex → CSS variables across 9 files

playlist.css, leaderBoard.css, BackToTop.css, ErrorFallback.jsx, search.css, header.css (modal), home.css (sponsors/features), PlayErrorBoundary.css


Note

Made all the changes locally but unable to start the dev server on my machine (Node v25 + react-scripts 5 compatibility issue). Please let me know if you can run it on your machine and verify the theme toggle works correctly.

… made changes but cant able to start the server in my machine tell me if you can run in your machine
Copilot AI review requested due to automatic review settings March 7, 2026 17:35
@netlify
Copy link

netlify bot commented Mar 7, 2026

Deploy Preview for reactplayio failed. Why did it fail? →

Name Link
🔨 Latest commit cded041
🔍 Latest deploy log https://app.netlify.com/projects/reactplayio/deploys/69ac61f967fb8d00080db7de

Copy link

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

Hey! contributor, thank you for opening a Pull Request 🎉.

@reactplay/maintainers will review your submission soon and give you helpful feedback. If you're interested in continuing your contributions to open source and want to be a part of a welcoming and fantastic community, we invite you to join our ReactPlay Discord Community.
Show your support by starring ⭐ this repository. Thank you and we appreciate your contribution to open source!
Stale Marking : After 30 days of inactivity this issue/PR will be marked as stale issue/PR and it will be closed and locked in 7 days if no further activity occurs.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds first-class dark/light theme support (Issue #1639) by introducing a theme context + early HTML theming to prevent FOUC, while also updating UI styles to use CSS variables and addressing the scrollbar track “white gap” issue.

Changes:

  • Introduces ThemeModeProvider and a header toggle to switch themes.
  • Adds an inline anti-FOUC script to set html[data-theme] before React mounts.
  • Updates global CSS variables + multiple component styles to be theme-aware and fixes scrollbar track styling.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
src/index.jsx Wraps the app with ThemeModeProvider so theme state is available globally.
src/common/theme/theme-context.jsx New theme context/provider handling theme state, persistence, and (intended) system sync.
public/index.html Inline script sets data-theme early to avoid flash of incorrect theme.
src/App.css Adds dark-theme variable overrides, body transitions, scrollbar styling changes, and global color defaults.
src/common/header/HeaderNav.jsx Adds the theme toggle button (sun/moon) wired to the context.
src/common/header/header.css Styles the theme toggle button and adjusts header colors for theming.
src/common/home/Home.jsx Wraps hero content for improved full-width layout behavior.
src/common/home/home.css Implements hero full-width fix and theme-specific hero gradients/text colors.
src/common/search/search.css Replaces hardcoded background with a neutral CSS variable.
src/common/playlists/playlist.css Replaces multiple hardcoded colors with CSS variables for theme support.
src/common/playlists/PlayErrorBoundary.css Converts error boundary styling colors to CSS variables.
src/common/playleaderboard/leaderBoard.css Converts leaderboard styling colors/shadows to CSS variables.
src/common/components/BackToTop.css Converts shadows/borders to use CSS variables.
src/ErrorBoundary/ErrorFallback.jsx Updates button background to use the brand CSS variable.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

background: #00f2fe;
color: #fff;
background: var(--color-brand-primary);
color: var(--color-neutral-90);
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

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

Using color: var(--color-neutral-90) on background: var(--color-brand-primary) will flip to light-on-light in dark mode because the neutral scale is inverted there, hurting contrast. Consider a theme-aware “on-brand-primary” text color (or explicit dark text) so the primary button stays readable in both themes.

Suggested change
color: var(--color-neutral-90);
color: var(--color-on-brand-primary, #000);

Copilot uses AI. Check for mistakes.
Comment on lines +54 to +57
--scrollbar-size: 12px;
--scrollbar-track: rgba(var(--color-neutral-10-rgb), 0.08);
--scrollbar-thumb: var(--color-brand-primary);

Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

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

--scrollbar-track and --scrollbar-thumb are defined here but the scrollbar rules below use transparent / var(--color-brand-primary) directly instead of these variables. Either wire the rules to the variables or remove the unused tokens to avoid confusion about the source of truth.

Suggested change
--scrollbar-size: 12px;
--scrollbar-track: rgba(var(--color-neutral-10-rgb), 0.08);
--scrollbar-thumb: var(--color-brand-primary);

Copilot uses AI. Check for mistakes.
Comment on lines +588 to +592

/* Dark mode header stays dark */
html[data-theme='dark'] .app-header {
background: #0b1120;
}
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

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

This dark-mode override hardcodes #0b1120, which duplicates the --color-neutral-10 value already set under html[data-theme='dark'] in App.css (and the header background is already var(--color-neutral-10)). Consider removing this rule or switching it to background: var(--color-neutral-10) to avoid redundant/duplicated theme constants.

Suggested change
/* Dark mode header stays dark */
html[data-theme='dark'] .app-header {
background: #0b1120;
}

Copilot uses AI. Check for mistakes.
Comment on lines +30 to +58
export const ThemeModeProvider = ({ children }) => {
const [theme, setTheme] = useState(getInitialTheme);

useEffect(() => {
applyThemeToDocument(theme);
window.localStorage.setItem(STORAGE_KEY, theme);
}, [theme]);

useEffect(() => {
// Keep in sync with system only until user picks explicitly.
const stored = window.localStorage.getItem(STORAGE_KEY);
if (stored === 'light' || stored === 'dark') return;

const mql = window.matchMedia?.('(prefers-color-scheme: dark)');
if (!mql) return;

const onChange = () => setTheme(getSystemTheme());
mql.addEventListener?.('change', onChange);

return () => mql.removeEventListener?.('change', onChange);
}, []);

const toggleTheme = useCallback(() => {
setTheme((t) => (t === 'dark' ? 'light' : 'dark'));
}, []);

const value = useMemo(() => ({ theme, setTheme, toggleTheme }), [theme, toggleTheme]);

return <ThemeModeContext.Provider value={value}>{children}</ThemeModeContext.Provider>;
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

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

Theme selection/persistence logic is non-trivial (stored value vs. system preference, and whether/when system changes should be observed) but there are no tests covering these behaviors. Adding unit tests for ThemeModeProvider/useThemeMode (initialization, toggle persistence, and system change syncing when no explicit preference exists) would help prevent regressions.

Copilot uses AI. Check for mistakes.
Comment on lines +39 to +41
var t = localStorage.getItem('reactplay.theme');
if (t !== 'light' && t !== 'dark') {
t = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

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

The inline anti-FOUC script assumes localStorage access always succeeds. In some environments (e.g., privacy modes or restricted storage), localStorage.getItem can throw and break the script. Wrap the localStorage read in a try/catch (and fall back to prefers-color-scheme) so a storage exception doesn’t prevent the theme attribute from being set.

Suggested change
var t = localStorage.getItem('reactplay.theme');
if (t !== 'light' && t !== 'dark') {
t = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
var t = null;
try {
t = localStorage.getItem('reactplay.theme');
} catch (e) {
t = null;
}
if (t !== 'light' && t !== 'dark') {
var m = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)');
t = m && m.matches ? 'dark' : 'light';

Copilot uses AI. Check for mistakes.
Comment on lines +13 to 17
background: 'var(--color-brand-primary)',
color: '#ffffff',
'&:hover': {
background: '#00f2fe',
background: 'var(--color-brand-primary)',
color: '#ffffff'
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

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

With dark mode enabled, --color-brand-primary is a light cyan, but this button text stays hardcoded to #ffffff, which will likely fail contrast requirements (light text on light background). Consider switching the text color to a theme-aware variable (e.g., an “on-brand-primary” token) so the button remains readable in both themes.

Copilot uses AI. Check for mistakes.
Comment on lines +33 to +42
useEffect(() => {
applyThemeToDocument(theme);
window.localStorage.setItem(STORAGE_KEY, theme);
}, [theme]);

useEffect(() => {
// Keep in sync with system only until user picks explicitly.
const stored = window.localStorage.getItem(STORAGE_KEY);
if (stored === 'light' || stored === 'dark') return;

Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

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

The provider currently writes theme into localStorage on every render via the [theme] effect. That makes the theme look like an explicit user preference immediately (even when it was derived from prefers-color-scheme), so the "sync with system until user picks" effect will never attach the matchMedia listener. Consider separating “effective theme” from “user preference” (e.g., store 'system'/null until user toggles), and only persist to localStorage when the user explicitly selects a mode.

Copilot uses AI. Check for mistakes.
Comment on lines +47 to +49
mql.addEventListener?.('change', onChange);

return () => mql.removeEventListener?.('change', onChange);
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

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

matchMedia change handling uses mql.addEventListener?.('change', ...) / removeEventListener, which won’t run on browsers that only support the older addListener/removeListener API. Given the production browserslist (">0.2%, not dead"), consider adding an addListener/removeListener fallback so system theme changes are reliably detected when no explicit preference is set.

Suggested change
mql.addEventListener?.('change', onChange);
return () => mql.removeEventListener?.('change', onChange);
if (typeof mql.addEventListener === 'function') {
mql.addEventListener('change', onChange);
return () => {
mql.removeEventListener('change', onChange);
};
}
if (typeof mql.addListener === 'function') {
mql.addListener(onChange);
return () => {
mql.removeListener(onChange);
};
}

Copilot uses AI. Check for mistakes.
background-color: #007bff;
color: #fff;
background-color: var(--color-brand-primary);
color: var(--color-neutral-90);
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

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

In dark theme, --color-neutral-90 becomes a very light color while --color-brand-primary is also light, so this button’s text will have poor contrast in dark mode. Consider introducing a dedicated “on-primary” text variable (overridden per theme) or otherwise ensure the button text remains dark enough against --color-brand-primary in both themes.

Suggested change
color: var(--color-neutral-90);
color: var(--color-on-brand-primary, var(--color-neutral-10));

Copilot uses AI. Check for mistakes.
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.

✨ [Feature request]: Add Dark And Light Mode And Some UI fix

3 participants