Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions reflex/.templates/web/utils/react-theme.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export function ThemeProvider({ children, defaultTheme = "system" }) {
const [systemTheme, setSystemTheme] = useState(
defaultTheme !== "system" ? defaultTheme : "light",
);
const [isInitialized, setIsInitialized] = useState(false);

const firstRender = useRef(true);

Expand All @@ -43,6 +44,7 @@ export function ThemeProvider({ children, defaultTheme = "system" }) {
// Load saved theme from localStorage
const savedTheme = localStorage.getItem("theme") || defaultTheme;
setTheme(savedTheme);
setIsInitialized(true);
});

const resolvedTheme = useMemo(
Expand All @@ -68,10 +70,12 @@ export function ThemeProvider({ children, defaultTheme = "system" }) {
};
});

// Save theme to localStorage whenever it changes
// Save theme to localStorage whenever it changes (but not on initial mount)
useEffect(() => {
localStorage.setItem("theme", theme);
}, [theme]);
if (isInitialized) {
localStorage.setItem("theme", theme);
}
}, [theme, isInitialized]);

useEffect(() => {
const root = window.document.documentElement;
Expand Down
6 changes: 6 additions & 0 deletions reflex/compiler/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,8 @@ def create_document_root(
Returns:
The document root.
"""
from reflex.utils.misc import preload_color_theme

existing_meta_types = set()

for component in head_components or []:
Expand All @@ -385,7 +387,11 @@ def create_document_root(
Meta.create(name="viewport", content="width=device-width, initial-scale=1")
)

# Add theme preload script as the very first component to prevent FOUC
theme_preload_components = [preload_color_theme()]

head_components = [
*theme_preload_components,
*(head_components or []),
*maybe_head_components,
*always_head_components,
Expand Down
35 changes: 35 additions & 0 deletions reflex/utils/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,38 @@ def with_cwd_in_syspath():
yield
finally:
sys.path[:] = orig_sys_path


def preload_color_theme():
"""Create a script component that preloads the color theme to prevent FOUC.

This script runs immediately in the document head before React hydration,
reading the saved theme from localStorage and applying the correct CSS classes
to prevent flash of unstyled content.

Returns:
Script: A script component to add to App.head_components
"""
from reflex.components.el.elements.scripts import Script

# Create direct inline script content (like next-themes dangerouslySetInnerHTML)
script_content = """
// Only run in browser environment, not during SSR
if (typeof document !== 'undefined') {
try {
const theme = localStorage.getItem("theme") || "system";
const systemPreference = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
const resolvedTheme = theme === "system" ? systemPreference : theme;

// Apply theme immediately - blocks until complete
document.documentElement.className = resolvedTheme;
document.documentElement.style.colorScheme = resolvedTheme;

} catch (e) {
// Fallback to light theme on any error
document.documentElement.className = "light";
}
}
"""

return Script.create(script_content)
Loading