Skip to content

Commit c266423

Browse files
fix(api): eliminate theme flash on docs.warp.dev/api (#6)
* fix(api): paint /api canvas synchronously to eliminate theme flash The /api page flashed white on first load before settling into dark mode. The head script already set <html data-theme> synchronously, but no CSS in <head> keyed off that attribute — the only theme-aware CSS lived inside Scalar's runtime-injected customCss (which keys off body classes .dark-mode / .light-mode), and that CSS only existed after the Scalar bundle (loaded mid-<body>) downloaded, parsed, and mounted. Add an inline <style> in <head> that paints html (and inheriting body) based on data-theme, so the canvas color is correct on the very first frame. Scalar's customCss still drives the rest of the page once it mounts. Co-Authored-By: Oz <oz-agent@warp.dev> * fix(api): boot Scalar in resolved theme to remove dark→light→dark flash The previous fix removed the white-on-first-frame flash but exposed a deeper timing bug: Scalar booted in light mode on cold loads, briefly flipped the page to light, then DOMContentLoaded restored dark. Root cause: the Scalar config sync script reads `document.body.classList.contains('dark-mode')` synchronously inside <body>, before the head script's body-class apply runs (which is queued for DOMContentLoaded because document.body is null in <head>). So cfg.darkMode was always false on cold load → Scalar mounted light → the mirror MutationObserver flipped html[data-theme]='light' → the new canvas CSS painted white → DOMContentLoaded fired and corrected everything. Fix: 1. Expose `window.__warpResolveTheme()` — a read-only sibling of the existing __warpApplyTheme that just resolves localStorage + prefers-color-scheme to 'dark' or 'light'. 2. Add a top-of-body inline script that calls __warpResolveTheme() and applies the body class synchronously, before any other body-level script runs. 3. Switch the Scalar config sync to read __warpResolveTheme() instead of body.classList, decoupling Scalar's boot value from DOM apply ordering. Co-Authored-By: Oz <oz-agent@warp.dev> --------- Co-authored-by: Oz <oz-agent@warp.dev>
1 parent c92f3c9 commit c266423

1 file changed

Lines changed: 64 additions & 4 deletions

File tree

src/pages/api.astro

Lines changed: 64 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,14 @@ const specJson = JSON.stringify(specObject);
9898
} catch (_e) {}
9999
apply(resolve(value === 'auto' ? '' : value));
100100
};
101+
// Read-only sibling of __warpApplyTheme: returns the resolved
102+
// 'dark' | 'light' value without touching the DOM or localStorage.
103+
// Used by the body-top inline script to apply the body class as
104+
// soon as <body> is parsed, and by Scalar's config sync script
105+
// to compute `cfg.darkMode` independently of body-class state.
106+
window.__warpResolveTheme = function () {
107+
return resolve(read());
108+
};
101109
// Re-resolve when the system preference changes, but only if
102110
// we're following it (stored value is empty / unknown).
103111
prefersDark.addEventListener('change', function () {
@@ -128,8 +136,48 @@ const specJson = JSON.stringify(specObject);
128136
else document.addEventListener('DOMContentLoaded', startMirror);
129137
})();
130138
</script>
139+
<style>
140+
/* First-paint canvas color, keyed off the `data-theme` attribute set
141+
synchronously by the script above. Without this, `/api` flashes
142+
white before mounting because Scalar's themed `customCss` (which
143+
keys off `body.dark-mode` / `body.light-mode`) only ships inside
144+
the Scalar bundle loaded mid-`<body>`, so nothing paints the
145+
page background until that bundle parses + mounts.
146+
147+
Values mirror `--scalar-background-1` from the customCss block
148+
below. If you change the canvas there (or in custom.css), update
149+
these in lockstep so the first-paint color matches what Scalar
150+
applies a few frames later. */
151+
html[data-theme="dark"] {
152+
background-color: #121212;
153+
color-scheme: dark;
154+
}
155+
html[data-theme="light"] {
156+
background-color: #ffffff;
157+
color-scheme: light;
158+
}
159+
body {
160+
background-color: inherit;
161+
}
162+
</style>
131163
</head>
132164
<body>
165+
<script is:inline>
166+
// Apply `body.dark-mode` / `body.light-mode` synchronously, before
167+
// any other body-level script runs (specifically, before the Scalar
168+
// config sync script and Scalar's bundle below). The <head> script
169+
// can't do this itself because `document.body` is null while <head>
170+
// parses; without this top-of-body apply, body would have no class
171+
// until DOMContentLoaded, Scalar would boot in light mode, the
172+
// mirror MutationObserver would briefly flip `html[data-theme]` to
173+
// `light`, and the page would visibly flash dark → light → dark.
174+
(function () {
175+
var resolved = (typeof window.__warpResolveTheme === 'function')
176+
? window.__warpResolveTheme()
177+
: 'dark';
178+
document.body.classList.add(resolved === 'dark' ? 'dark-mode' : 'light-mode');
179+
})();
180+
</script>
133181
<WarpTopbar crumb="API Reference" />
134182
<script
135183
is:inline
@@ -254,17 +302,29 @@ const specJson = JSON.stringify(specObject);
254302
<script is:inline>
255303
// Sync Scalar's `darkMode` config to the resolved theme before the
256304
// Scalar bundle reads `data-configuration`. This keeps Scalar's
257-
// internal `useColorMode` state aligned with the body class our
258-
// <head> script already set, eliminating the one-frame mismatch
259-
// where Scalar would briefly mount in its own default.
305+
// internal `useColorMode` state aligned with the resolved theme,
306+
// eliminating the one-frame mismatch where Scalar would briefly
307+
// mount in its own default.
308+
//
309+
// We read directly from `__warpResolveTheme()` (which resolves
310+
// `localStorage['starlight-theme']` + `prefers-color-scheme`)
311+
// rather than from `document.body.classList`, because the body
312+
// class is intentionally not a reliable boot-time source: the
313+
// <head> script can't apply it before <body> exists, and the
314+
// top-of-body apply runs in the same parser frame as us. Reading
315+
// localStorage decouples Scalar's boot value from any DOM apply
316+
// ordering, so dark-mode users never see a transient light boot.
260317
(function () {
261318
var el = document.getElementById('api-reference');
262319
if (!el) return;
263320
var raw = el.getAttribute('data-configuration');
264321
if (!raw) return;
265322
try {
266323
var cfg = JSON.parse(raw);
267-
cfg.darkMode = document.body.classList.contains('dark-mode');
324+
var resolved = (typeof window.__warpResolveTheme === 'function')
325+
? window.__warpResolveTheme()
326+
: (document.body.classList.contains('dark-mode') ? 'dark' : 'light');
327+
cfg.darkMode = resolved === 'dark';
268328
el.setAttribute('data-configuration', JSON.stringify(cfg));
269329
} catch (_e) {
270330
// Malformed config — let Scalar fall back to its own default.

0 commit comments

Comments
 (0)