Skip to content

Commit 0b53971

Browse files
fix: resolve flash of unstyled content (FOUC) in dark mode
1 parent 3d00fa8 commit 0b53971

File tree

3 files changed

+42
-2
lines changed

3 files changed

+42
-2
lines changed

src/generators/web/template.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono&family=Open+Sans:ital,wght@0,300..800;1,300..800" />
2020

2121
<!-- Apply theme before paint to avoid Flash of Unstyled Content -->
22-
<script>document.documentElement.setAttribute("data-theme", document.documentElement.style.colorScheme = localStorage.getItem("theme") || (matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"));</script>
22+
<script >${themeScript}</script>
2323
<script type="importmap">${importMap}</script>
2424
<script type="speculationrules">${speculationRules}</script>
2525
</head>
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
'use strict';
2+
3+
/**
4+
* This script is designed to be inlined in the <head> of the HTML template.
5+
* It must execute BEFORE the body renders to prevent a Flash of Unstyled Content (FOUC).
6+
*/
7+
function initializeTheme() {
8+
const THEME_STORAGE_KEY = 'theme';
9+
const THEME_DATA_ATTRIBUTE = 'data-theme';
10+
const DARK_QUERY = '(prefers-color-scheme: dark)';
11+
12+
// 1. Retrieve the user's preference from localStorage
13+
const savedUserPreference = localStorage.getItem(THEME_STORAGE_KEY);
14+
15+
// 2. Determine if the system/browser is currently set to dark mode
16+
const systemSupportsDarkMode = window.matchMedia(DARK_QUERY).matches;
17+
18+
/**
19+
* 3. Logic to determine if 'dark' should be applied:
20+
* - User explicitly saved 'dark'
21+
* - User set preference to 'system' AND system is dark
22+
* - No preference exists yet AND system is dark
23+
*/
24+
const shouldApplyDark =
25+
savedUserPreference === 'dark' ||
26+
(savedUserPreference === 'system' && systemSupportsDarkMode) ||
27+
(!savedUserPreference && systemSupportsDarkMode);
28+
29+
const themeToApply = shouldApplyDark ? 'dark' : 'light';
30+
31+
// 4. Apply the theme attribute to the document element (<html>)
32+
document.documentElement.setAttribute(THEME_DATA_ATTRIBUTE, themeToApply);
33+
34+
// 5. Set color-scheme to ensure browser UI (scrollbars, etc.) matches the theme
35+
document.documentElement.style.colorScheme = themeToApply;
36+
}
37+
38+
// Export the function body wrapped in an IIFE string
39+
export default `(${initializeTheme.toString()})();`;

src/generators/web/utils/processing.mjs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import getConfig from '../../../utils/configuration/index.mjs';
1313
import { populate } from '../../../utils/configuration/templates.mjs';
1414
import { minifyHTML } from '../../../utils/html-minifier.mjs';
1515
import { SPECULATION_RULES } from '../constants.mjs';
16-
16+
import themeScript from '../ui/theme-script.mjs?raw';
1717
/**
1818
* Populates a template string by evaluating it as a JavaScript template literal,
1919
* allowing full JS expression syntax (e.g., ${if ...}, ${JSON.stringify(...)}).
@@ -137,6 +137,7 @@ export async function processJSXEntries(entries, template) {
137137
importMap: clientBundle.importMap?.replaceAll('/', root) ?? '',
138138
entrypoint: `${data.api}.js?${randomUUID()}`,
139139
speculationRules: SPECULATION_RULES,
140+
themeScript,
140141
root,
141142
metadata: data,
142143
config,

0 commit comments

Comments
 (0)