Skip to content

Commit 7c0c1df

Browse files
authored
fix: storybook initial theme for vanilla provider
1 parent 79b6f21 commit 7c0c1df

File tree

3 files changed

+59
-47
lines changed

3 files changed

+59
-47
lines changed

.storybook/preview.jsx

Lines changed: 47 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import { themes } from 'storybook/theming';
77
import { globalCss } from '../';
88
import { FaencyProvider } from '../components/FaencyProvider';
99
import { darkTheme, lightTheme } from '../stitches.config';
10+
import { VanillaExtractThemeProvider } from '../styles/themeContext';
11+
import { themes as vanillaThemes } from '../styles/themes.css';
1012

1113
const channel = addons.getChannel();
1214

@@ -17,21 +19,42 @@ const channel = addons.getChannel();
1719
const getInitialThemePreference = () => {
1820
if (typeof window === 'undefined') return false;
1921

20-
// Check stored preference first
22+
// Check stored preference
2123
const stored = localStorage.getItem('sb-addon-themes-3');
2224
if (stored) {
2325
try {
2426
const parsed = JSON.parse(stored);
25-
if (parsed.current === 'dark') return true;
26-
} catch (e) {
27-
// Fall through to system preference
27+
if (parsed.current) {
28+
return parsed.current === 'dark';
29+
}
30+
} catch (error) {
31+
console.warn('Failed to parse Storybook theme preference:', error);
2832
}
2933
}
3034

31-
// Fallback to system preference
35+
// Fallback to system preference only if no stored value or parse error
3236
return window.matchMedia('(prefers-color-scheme: dark)').matches;
3337
};
3438

39+
// Initialize vanilla-extract theme class immediately on script load
40+
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
41+
const initialTheme = getInitialThemePreference() ? 'dark' : 'light';
42+
const themeClass = vanillaThemes[initialTheme]['blue'];
43+
44+
function applyInitialTheme() {
45+
Object.values(vanillaThemes.light).forEach((cls) => document.body.classList.remove(cls));
46+
Object.values(vanillaThemes.dark).forEach((cls) => document.body.classList.remove(cls));
47+
48+
document.body.classList.add(themeClass);
49+
}
50+
51+
if (document.body) {
52+
applyInitialTheme();
53+
} else {
54+
document.addEventListener('DOMContentLoaded', applyInitialTheme);
55+
}
56+
}
57+
3558
export const parameters = {
3659
controls: {
3760
matchers: {
@@ -72,38 +95,14 @@ const globalStyle = globalCss({
7295
});
7396

7497
const VanillaProviderWrapper = ({ children, isDark, primaryColor }) => {
75-
const [Provider, setProvider] = React.useState(null);
76-
const [loading, setLoading] = React.useState(true);
77-
78-
React.useEffect(() => {
79-
import('../styles/themeContext')
80-
.then((module) => {
81-
setProvider(() => module.VanillaExtractThemeProvider);
82-
setLoading(false);
83-
})
84-
.catch((err) => {
85-
console.warn('VanillaExtractThemeProvider failed to load:', err);
86-
setLoading(false);
87-
});
88-
}, []);
89-
90-
if (loading) {
91-
return React.createElement('div', { style: { padding: '24px' } }, 'Loading theme system...');
92-
}
93-
94-
if (Provider) {
95-
return React.createElement(
96-
Provider,
97-
{
98-
forcedTheme: isDark ? 'dark' : 'light',
99-
primaryColor,
100-
},
101-
children,
102-
);
103-
}
104-
105-
// If provider failed to load, just return children (Stitches fallback)
106-
return children;
98+
return React.createElement(
99+
VanillaExtractThemeProvider,
100+
{
101+
forcedTheme: isDark ? 'dark' : 'light',
102+
primaryColor,
103+
},
104+
children,
105+
);
107106
};
108107

109108
export const decorators = [
@@ -120,6 +119,17 @@ export const decorators = [
120119
return () => channel.removeListener(DARK_MODE_EVENT_NAME, setDark);
121120
}, []);
122121

122+
// Apply vanilla-extract theme class before first render to prevent flash
123+
React.useLayoutEffect(() => {
124+
const resolvedTheme = isDark ? 'dark' : 'light';
125+
const themeClass = vanillaThemes[resolvedTheme]['blue'];
126+
127+
Object.values(vanillaThemes.light).forEach((cls) => document.body.classList.remove(cls));
128+
Object.values(vanillaThemes.dark).forEach((cls) => document.body.classList.remove(cls));
129+
130+
document.body.classList.add(themeClass);
131+
}, [isDark]);
132+
123133
return (
124134
<VanillaProviderWrapper isDark={isDark} primaryColor="blue">
125135
<FaencyProvider>

stories/ThemeTest.stories.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,3 @@ export const ThemeAPITest: StoryFn = () => {
223223

224224
return <ThemeInfo />;
225225
};
226-
227-
PrimaryColorShowcase.storyName = 'Primary Color Showcase';
228-
ThemeAPITest.storyName = 'Theme API Test';

styles/themeContext.tsx

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { createContext, useContext, useEffect, useState } from 'react';
1+
import React, { createContext, useContext, useEffect, useLayoutEffect, useState } from 'react';
22

33
import { darkColors, lightColors } from '../colors';
44
import { PrimaryColor, themes } from './themes.css';
@@ -33,7 +33,9 @@ export function VanillaExtractThemeProvider({
3333
const [mode, setMode] = useState<ThemeMode>(defaultTheme);
3434
const [appliedPrimaryColor, setPrimaryColor] = useState<PrimaryColor>(primaryColor);
3535
const [systemTheme, setSystemTheme] = useState<'light' | 'dark'>(() => {
36-
// Initialize with actual system preference to avoid flash
36+
if (forcedTheme) {
37+
return forcedTheme;
38+
}
3739
if (typeof window !== 'undefined') {
3840
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
3941
}
@@ -45,6 +47,11 @@ export function VanillaExtractThemeProvider({
4547
}, [primaryColor]);
4648

4749
useEffect(() => {
50+
if (forcedTheme) {
51+
setSystemTheme(forcedTheme);
52+
return;
53+
}
54+
4855
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
4956
setSystemTheme(mediaQuery.matches ? 'dark' : 'light');
5057

@@ -54,13 +61,12 @@ export function VanillaExtractThemeProvider({
5461

5562
mediaQuery.addEventListener('change', handler);
5663
return () => mediaQuery.removeEventListener('change', handler);
57-
}, []);
64+
}, [forcedTheme]);
5865

5966
const resolvedTheme: 'light' | 'dark' = forcedTheme || (mode === 'system' ? systemTheme : mode);
6067

6168
const baseColors = resolvedTheme === 'dark' ? darkColors : lightColors;
6269

63-
// Add semantic colors based on the current theme and primary color
6470
const semanticColors =
6571
resolvedTheme === 'dark'
6672
? {
@@ -115,14 +121,13 @@ export function VanillaExtractThemeProvider({
115121
...semanticColors,
116122
};
117123

118-
useEffect(() => {
124+
// Use useLayoutEffect to apply theme class synchronously before paint
125+
useLayoutEffect(() => {
119126
const themeClass = themes[resolvedTheme][appliedPrimaryColor];
120127

121-
// Remove all theme classes
122128
Object.values(themes.light).forEach((cls) => document.body.classList.remove(cls));
123129
Object.values(themes.dark).forEach((cls) => document.body.classList.remove(cls));
124130

125-
// Add current theme class
126131
document.body.classList.add(themeClass);
127132

128133
return () => {

0 commit comments

Comments
 (0)