Skip to content

Commit 1bfa4a0

Browse files
committed
🤖 fix: prevent nested ThemeProvider from overriding forced theme
Root cause: When running in Storybook with Chromatic modes, the decorator wraps stories in ThemeProvider with forcedTheme. But App.tsx also has its own ThemeProvider without forcedTheme. The inner provider's useLayoutEffect was overwriting the outer forced theme with the browser's prefers-color-scheme. Solution: ThemeProvider now tracks whether it has a forced theme via isForced context value. Nested providers check parentContext.isForced and defer to the parent's theme when true, preventing the inner provider from overwriting. Verified locally using Playwright: - Browser prefers light + globals=theme:dark → correctly shows dark - Browser prefers dark + globals=theme:light → correctly shows light - Background colors correctly differ between themes _Generated with `mux`_
1 parent 075e156 commit 1bfa4a0

File tree

1 file changed

+25
-6
lines changed

1 file changed

+25
-6
lines changed

src/browser/contexts/ThemeContext.tsx

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ interface ThemeContextValue {
1515
theme: ThemeMode;
1616
setTheme: React.Dispatch<React.SetStateAction<ThemeMode>>;
1717
toggleTheme: () => void;
18+
/** True if this provider has a forcedTheme - nested providers should not override */
19+
isForced: boolean;
1820
}
1921

2022
const ThemeContext = createContext<ThemeContextValue | null>(null);
@@ -58,6 +60,10 @@ export function ThemeProvider({
5860
children: ReactNode;
5961
forcedTheme?: ThemeMode;
6062
}) {
63+
// Check if we're nested inside a forced theme provider
64+
const parentContext = useContext(ThemeContext);
65+
const isNestedUnderForcedProvider = parentContext?.isForced ?? false;
66+
6167
const [persistedTheme, setTheme] = usePersistedState<ThemeMode>(
6268
UI_THEME_KEY,
6369
resolveSystemTheme(),
@@ -66,23 +72,36 @@ export function ThemeProvider({
6672
}
6773
);
6874

69-
const theme = forcedTheme ?? persistedTheme;
75+
// If nested under a forced provider, use parent's theme
76+
// Otherwise, use forcedTheme (if provided) or persistedTheme
77+
const theme =
78+
isNestedUnderForcedProvider && parentContext
79+
? parentContext.theme
80+
: (forcedTheme ?? persistedTheme);
81+
82+
const isForced = forcedTheme !== undefined || isNestedUnderForcedProvider;
7083

84+
// Only apply to document if we're the authoritative provider
7185
useLayoutEffect(() => {
72-
applyThemeToDocument(theme);
73-
}, [theme]);
86+
if (!isNestedUnderForcedProvider) {
87+
applyThemeToDocument(theme);
88+
}
89+
}, [theme, isNestedUnderForcedProvider]);
7490

7591
const toggleTheme = useCallback(() => {
76-
setTheme((current) => (current === "dark" ? "light" : "dark"));
77-
}, [setTheme]);
92+
if (!isNestedUnderForcedProvider) {
93+
setTheme((current) => (current === "dark" ? "light" : "dark"));
94+
}
95+
}, [setTheme, isNestedUnderForcedProvider]);
7896

7997
const value = useMemo<ThemeContextValue>(
8098
() => ({
8199
theme,
82100
setTheme,
83101
toggleTheme,
102+
isForced,
84103
}),
85-
[setTheme, theme, toggleTheme]
104+
[setTheme, theme, toggleTheme, isForced]
86105
);
87106

88107
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;

0 commit comments

Comments
 (0)