zephyrie Claude Opus 4.7 commited on
Commit
68d0499
·
1 Parent(s): 89c8f24

Fix light mode by collapsing the theme, not hacking the DOM

Browse files

The previous fix forced Gradio's .dark class via a <head> script +
MutationObserver. That fought Gradio's SSR hydration and triggered a
remount loop: the hero cards (gr.HTML) never rendered and the page was
left with three dead buttons.

Root cause of the original bug: Gradio's _get_theme_css() emits every
token under both :root (light) and :root.dark (dark). Our UI is
dark-native, so light mode looked washed out.

Real fix: build the theme with gr.themes.Base(), then copy every *_dark
token value onto its light counterpart. Gradio's :root block then
carries dark colors and the app renders dark for every visitor -- pure
server-rendered CSS, no client-side script, nothing to break hydration.
Verified against gradio 6.14.0: 84 tokens collapsed, server serves
theme.css with dark values in :root, build_app() and launch() clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

Files changed (1) hide show
  1. app.py +26 -42
app.py CHANGED
@@ -1425,45 +1425,30 @@ footer { display: none !important; }
1425
  """
1426
 
1427
 
1428
- # The UI is dark-mode-native (navy "MAISI Console" aesthetic, no light variant).
1429
- # Gradio drives light/dark by toggling a `.dark` class on the DOM; HF Spaces'
1430
- # own theme switch can remove it. We pin the class on every plausible host
1431
- # element and re-add it via MutationObserver whenever it's stripped, so the app
1432
- # always renders dark regardless of OS preference or the HF theme toggle.
1433
- _FORCE_DARK_BODY = """
1434
- function force() {
1435
- document.documentElement.classList.add('dark');
1436
- if (document.body) document.body.classList.add('dark');
1437
- document.querySelectorAll('gradio-app').forEach(function (el) {
1438
- el.classList.add('dark');
1439
- });
1440
- }
1441
- force();
1442
- new MutationObserver(force).observe(document.documentElement, {
1443
- attributes: true, attributeFilter: ['class']
1444
- });
1445
- function watchBody() {
1446
- force();
1447
- if (document.body) {
1448
- new MutationObserver(force).observe(document.body, {
1449
- attributes: true, attributeFilter: ['class']
1450
- });
1451
- }
1452
- }
1453
- if (document.readyState === 'loading') {
1454
- document.addEventListener('DOMContentLoaded', watchBody);
1455
- } else {
1456
- watchBody();
1457
- }
1458
- """
1459
-
1460
- FORCE_DARK_HEAD = (
1461
- '<meta name="color-scheme" content="dark">'
1462
- "<script>(function () {" + _FORCE_DARK_BODY + "})();</script>"
1463
- )
1464
-
1465
- # Re-assert once more after the Gradio app finishes loading (belt-and-suspenders).
1466
- FORCE_DARK_JS = "() => {" + _FORCE_DARK_BODY + "}"
1467
 
1468
 
1469
  def build_app() -> gr.Blocks:
@@ -1499,7 +1484,6 @@ if __name__ == "__main__":
1499
  server_port=int(os.environ.get("PORT", "7860")),
1500
  show_error=True,
1501
  css=CSS,
1502
- theme=gr.themes.Base(),
1503
- js=FORCE_DARK_JS,
1504
- head=FORCE_DARK_HEAD,
1505
  )
 
1425
  """
1426
 
1427
 
1428
+ # The UI is dark-mode-native (navy "MAISI Console" aesthetic) there is no
1429
+ # light variant. Rather than fight Gradio's light/dark toggle at the DOM level
1430
+ # (DOM/observer hacks break SSR hydration), we collapse the theme itself:
1431
+ # Gradio's _get_theme_css() emits every token twice under `:root` (light) and
1432
+ # `:root.dark` (dark). By copying each token's dark value onto its light
1433
+ # counterpart, `:root` carries dark colors too, so the app renders dark
1434
+ # regardless of OS preference or the HF Spaces theme switch. Pure CSS output —
1435
+ # nothing client-side to break.
1436
+ def _all_dark_theme() -> gr.themes.Base:
1437
+ theme = gr.themes.Base()
1438
+ for attr in list(theme.__dict__):
1439
+ if attr.startswith("_") or not attr.endswith("_dark"):
1440
+ continue
1441
+ dark_val = theme.__dict__[attr]
1442
+ if dark_val is None: # None => dark inherits light; leave light as-is
1443
+ continue
1444
+ light_attr = attr[: -len("_dark")]
1445
+ if light_attr in theme.__dict__:
1446
+ theme.__dict__[light_attr] = dark_val
1447
+ return theme
1448
+
1449
+
1450
+ # Keep the browser's native chrome (scrollbars, form controls) dark too.
1451
+ DARK_HEAD = '<meta name="color-scheme" content="dark">'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1452
 
1453
 
1454
  def build_app() -> gr.Blocks:
 
1484
  server_port=int(os.environ.get("PORT", "7860")),
1485
  show_error=True,
1486
  css=CSS,
1487
+ theme=_all_dark_theme(),
1488
+ head=DARK_HEAD,
 
1489
  )