Apply theme synchronously before first paint

#9
by volivers - opened
Files changed (1) hide show
  1. app.py +70 -9
app.py CHANGED
@@ -2383,6 +2383,46 @@ ATTRIBUTION_HTML = """
2383
  """
2384
 
2385
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2386
  # JS injected once; the buttons call pickInstrument() which writes the chosen
2387
  # value into the hidden Gradio textbox for that group and triggers its change.
2388
  SELECTION_JS = """
@@ -2393,6 +2433,11 @@ SELECTION_JS = """
2393
  document.body.classList.toggle('dark', dark);
2394
  const container = document.querySelector('.gradio-container');
2395
  if (container) container.classList.toggle('dark', dark);
 
 
 
 
 
2396
  }
2397
 
2398
  function toggleDark() {
@@ -2409,14 +2454,22 @@ SELECTION_JS = """
2409
  window._lolaby_applyTheme = applyTheme;
2410
  window._lolaby_toggleDark = toggleDark;
2411
 
2412
- window.addEventListener('load', function() {
2413
- const url = new URL(window.location);
2414
- const isBrowserDark = window.matchMedia?.('(prefers-color-scheme: dark)').matches ?? false;
2415
- const noTheme = !url.searchParams.has('__theme');
2416
- const isDark = noTheme ? isBrowserDark : url.searchParams.get('__theme') === 'dark';
2417
-
 
 
2418
  applyTheme(isDark);
2419
- });
 
 
 
 
 
 
2420
  })();
2421
 
2422
  // Ordered multi-select picker. Up to 2 instruments.
@@ -2590,8 +2643,16 @@ function flashComboHint() {
2590
  """
2591
 
2592
 
 
 
 
 
 
 
 
 
2593
  with gr.Blocks(css_paths="style.css", title="Lolaby", theme=gr.themes.Citrus(),
2594
- head=f"<script>{SELECTION_JS}</script>") as demo:
2595
  gr.HTML("""
2596
  <div style="text-align:right; padding: 12px 18px 0;">
2597
  <label class="dark-toggle-label">
@@ -3051,7 +3112,7 @@ gr.mount_gradio_app(
3051
  path="/",
3052
  theme=gr.themes.Citrus(),
3053
  css_paths="style.css",
3054
- head=f"<script>{SELECTION_JS}</script>",
3055
  )
3056
 
3057
 
 
2383
  """
2384
 
2385
 
2386
+ # ---------------------------------------------------------------------------
2387
+ # EARLY_THEME_JS β€” must run as the FIRST thing in <head>, synchronously,
2388
+ # before any CSS/markup is painted.
2389
+ #
2390
+ # Why: the old approach decided light vs. dark inside a
2391
+ # `window.addEventListener('load', ...)` handler. `load` fires only after
2392
+ # the ENTIRE page (styles, fonts, the whole Gradio bundle) has finished
2393
+ # loading β€” by which point the page has already been painted once in the
2394
+ # default (light) theme. For anyone whose browser/OS prefers dark mode (or
2395
+ # who has `?__theme=dark` in the URL), every reload showed a flash of the
2396
+ # light theme that then snapped/animated to dark β€” the reported "flicker".
2397
+ #
2398
+ # Fix: read the theme preference (URL param > OS preference) and add the
2399
+ # `.dark` class to <html> immediately, in a blocking inline script placed
2400
+ # before any stylesheet. Since all theme tokens live on `:root`/`.dark`
2401
+ # (style.css), this means the very first paint already has the correct
2402
+ # variables β€” there's nothing to flip after the fact, so no flash and no
2403
+ # transition to animate.
2404
+ # ---------------------------------------------------------------------------
2405
+ EARLY_THEME_JS = """
2406
+ (function() {
2407
+ try {
2408
+ var url = new URL(window.location.href);
2409
+ var isDark;
2410
+ if (url.searchParams.has('__theme')) {
2411
+ isDark = url.searchParams.get('__theme') === 'dark';
2412
+ } else {
2413
+ isDark = !!(window.matchMedia &&
2414
+ window.matchMedia('(prefers-color-scheme: dark)').matches);
2415
+ }
2416
+ if (isDark) {
2417
+ document.documentElement.classList.add('dark');
2418
+ }
2419
+ } catch (e) {
2420
+ // If anything above throws (e.g. matchMedia unavailable), just fall
2421
+ // back to the light-mode default rather than breaking the page.
2422
+ }
2423
+ })();
2424
+ """
2425
+
2426
  # JS injected once; the buttons call pickInstrument() which writes the chosen
2427
  # value into the hidden Gradio textbox for that group and triggers its change.
2428
  SELECTION_JS = """
 
2433
  document.body.classList.toggle('dark', dark);
2434
  const container = document.querySelector('.gradio-container');
2435
  if (container) container.classList.toggle('dark', dark);
2436
+ // Keep the visible toggle switch in sync with the active theme β€”
2437
+ // without this, a theme applied via EARLY_THEME_JS (system
2438
+ // preference or ?__theme=dark) left the switch showing "off".
2439
+ const toggleBtn = document.getElementById('dark-toggle-btn');
2440
+ if (toggleBtn) toggleBtn.checked = dark;
2441
  }
2442
 
2443
  function toggleDark() {
 
2454
  window._lolaby_applyTheme = applyTheme;
2455
  window._lolaby_toggleDark = toggleDark;
2456
 
2457
+ // EARLY_THEME_JS already set the `.dark` class on <html> (synchronously,
2458
+ // before first paint) if needed. Here we just propagate that class to
2459
+ // <body>/.gradio-container and sync the toggle switch β€” and we do it on
2460
+ // DOMContentLoaded (as soon as the DOM exists) rather than `load` (which
2461
+ // waits for every image/font/asset), so there's no extra delay where the
2462
+ // body/container could render with mismatched classes.
2463
+ function syncThemeClasses() {
2464
+ const isDark = document.documentElement.classList.contains('dark');
2465
  applyTheme(isDark);
2466
+ }
2467
+
2468
+ if (document.readyState === 'loading') {
2469
+ document.addEventListener('DOMContentLoaded', syncThemeClasses);
2470
+ } else {
2471
+ syncThemeClasses();
2472
+ }
2473
  })();
2474
 
2475
  // Ordered multi-select picker. Up to 2 instruments.
 
2643
  """
2644
 
2645
 
2646
+ # EARLY_THEME_JS comes FIRST (before css_paths/style.css is linked in the
2647
+ # head) so the .dark class is set before any themed CSS rule is evaluated β€”
2648
+ # that's what eliminates the light->dark flash on reload. SELECTION_JS comes
2649
+ # after; it's fine for it to load alongside/after stylesheets since it only
2650
+ # touches non-paint-critical bookkeeping (button wiring, class sync, etc.).
2651
+ HEAD_HTML = f"<script>{EARLY_THEME_JS}</script><script>{SELECTION_JS}</script>"
2652
+
2653
+
2654
  with gr.Blocks(css_paths="style.css", title="Lolaby", theme=gr.themes.Citrus(),
2655
+ head=HEAD_HTML) as demo:
2656
  gr.HTML("""
2657
  <div style="text-align:right; padding: 12px 18px 0;">
2658
  <label class="dark-toggle-label">
 
3112
  path="/",
3113
  theme=gr.themes.Citrus(),
3114
  css_paths="style.css",
3115
+ head=HEAD_HTML,
3116
  )
3117
 
3118