-
Notifications
You must be signed in to change notification settings - Fork 46
fix: resolve flash of unstyled content in dark mode #766
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,7 +15,15 @@ import { writeFile } from '../../utils/file.mjs'; | |
| export async function generate(input) { | ||
| const config = getConfig('web'); | ||
|
|
||
| const template = await readFile(config.templatePath, 'utf-8'); | ||
| let template = await readFile(config.templatePath, 'utf-8'); | ||
|
|
||
| // Read the theme script from UI folder | ||
| const themeScriptPath = join(import.meta.dirname, 'ui', 'theme-script.mjs'); | ||
| const themeScriptContent = await readFile(themeScriptPath, 'utf-8'); | ||
|
|
||
| // Wrap theme script in script tag and replace the placeholder | ||
| const inlinedThemeScript = `<script>${themeScriptContent}</script>`; | ||
| template = template.replace('${themeScript}', inlinedThemeScript); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't use .replace(), I believe that @avivkeller implemented a system to automatically evaluate such template variables? |
||
|
|
||
| // Process all entries: convert JSX to HTML/CSS/JS | ||
| const { results, css, chunks } = await processJSXEntries(input, template); | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -18,8 +18,12 @@ | |||||
| <link rel="stylesheet" | ||||||
| href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono&family=Open+Sans:ital,wght@0,300..800;1,300..800" /> | ||||||
|
|
||||||
| <!-- Apply theme before paint to avoid Flash of Unstyled Content --> | ||||||
| <script>document.documentElement.setAttribute("data-theme", document.documentElement.style.colorScheme = localStorage.getItem("theme") || (matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"));</script> | ||||||
| <!-- | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No need for this comment. |
||||||
| This placeholder is dynamically replaced by generator.mjs. | ||||||
| It injects an inline script that initializes the theme (light/dark) | ||||||
| before the page renders to avoid Flash of Unstyled Content (FOUC). | ||||||
| --> | ||||||
| ${themeScript} | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| <script type="importmap">${importMap}</script> | ||||||
| <script type="speculationrules">${speculationRules}</script> | ||||||
| </head> | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| 'use strict'; | ||
|
|
||
| /** | ||
| * This script is designed to be inlined in the <head> of the HTML template. | ||
| * It must execute BEFORE the body renders to prevent a Flash of Unstyled Content (FOUC). | ||
| */ | ||
| (function initializeTheme() { | ||
| const THEME_STORAGE_KEY = 'theme'; | ||
| const THEME_DATA_ATTRIBUTE = 'data-theme'; | ||
| const DARK_QUERY = '(prefers-color-scheme: dark)'; | ||
|
|
||
| // 1. Retrieve the user's preference from localStorage | ||
| const savedUserPreference = localStorage.getItem(THEME_STORAGE_KEY); | ||
|
|
||
| // 2. Determine if the system/browser is currently set to dark mode | ||
| const systemSupportsDarkMode = window.matchMedia(DARK_QUERY).matches; | ||
|
|
||
| /** | ||
| * 3. Logic to determine if 'dark' should be applied: | ||
| * - User explicitly saved 'dark' | ||
| * - User set preference to 'system' AND system is dark | ||
| * - No preference exists yet AND system is dark | ||
| */ | ||
| const shouldApplyDark = | ||
| savedUserPreference === 'dark' || | ||
| (savedUserPreference === 'system' && systemSupportsDarkMode) || | ||
| (!savedUserPreference && systemSupportsDarkMode); | ||
|
|
||
| const themeToApply = shouldApplyDark ? 'dark' : 'light'; | ||
|
|
||
| // 4. Apply the theme attribute to the document element (<html>) | ||
| document.documentElement.setAttribute(THEME_DATA_ATTRIBUTE, themeToApply); | ||
|
|
||
| // 5. Set color-scheme to ensure browser UI (scrollbars, etc.) matches the theme | ||
| document.documentElement.style.colorScheme = themeToApply; | ||
| })(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We shouldn't ue
fsthe idea is to use our rolldown pipeline for this script, so it gets properly minified and transformed.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Otherwise the file doesn't get minified, which is the idea behind separating src<>final result.