python-docs-theme icon indicating copy to clipboard operation
python-docs-theme copied to clipboard

Set CSS value in HTML to reflect current theme

Open vsajip opened this issue 1 year ago • 9 comments

See this page, where the anchor points to an SVG. If you ensure that there's no theme in local storage for this page, then if the browser settings are used to switch between dark and light modes (I'm using Firefox), the SVG is styled appropriately to display correctly. However, if I use the theme selector on the page to select dark mode when the browser is set to light mode, the SVG does not display correctly.

The reason is that I style for light mode and use a media query to override some styles, using @media (prefers-color-scheme: dark). However, when the theme selector is used to select dark mode, then some dark-mode CSS is pulled in using the activateTheme() JS function, but from what I can tell this leaves no trace in the markup that we're now in dark mode. If there were e.g. a dark-theme class added to the <body> tag when the page is in dark mode, then I could duplicate the styling using an appropriate selector. My complete styling of the SVG is:

    svg {
      background-color: transparent !important;
    }
    line {
      stroke: #000000;
      fill: none;
      stroke-opacity: 1;
    }
    polygon, rect {
      fill: none;
      stroke: #000000;
      fill-opacity: 1;
      stroke-opacity: 1;
    }
    polygon.filled {
      fill: #ff0000;
    }
    polyline {
      fill: none;
      stroke-opacity: 1;
      stroke: #000000;
    }
    text {
      fill: #000000;
      fill-opacity: 1;
      stroke: none;
      font-family: sans-serif;
      font-style: normal;
      font-weight: normal;
      text-anchor: start;
    }
    @media (prefers-color-scheme: dark) { /* could duplicate this styling if body.dark-theme */
      polygon, rect, polyline, line {
        stroke: #ffffff;
      }
      text {
        fill: #ffffff;
      }
    }

If there is another approach I could use to achieve the desired result (proper styling of the SVG in all cases), please let me know. Otherwise, would you consider adding dark-theme class to the body or other container tag so that I could try and achieve the desired result? Or have I missed somewhere where such a class has been added?

vsajip avatar Jul 01 '24 18:07 vsajip

I got this working by overwriting activateTheme with my own function, see here. The result can be previewed here. It would still be good to have the function of setting/unsetting body.{light,dark}-theme in this theme.

vsajip avatar Jul 03 '24 19:07 vsajip

Yeah, https://peps.python.org/ has a data-colour_scheme attribute on the html element that changes between dark, light and auto based on the theme selection.

I'd welcome a PR to do something like this here too.

hugovk avatar Jul 21 '24 20:07 hugovk

Do you want it to match the peps.python.org behaviour exactly? Or is what I've implemented (using the body tag, and the attribute has a -theme suffix) OK?

vsajip avatar Jul 22 '24 13:07 vsajip

We don't need to copy peps.python.org exactly.

Checking a couple of others:

Furo has a <body data-theme="dark"> which toggles the three states.

PyData Sphinx Theme has the same thing, but it only togges light and dark. It also has a data-mode that toggles the thee states.

Hmm, shall we follow Furo?

hugovk avatar Jul 22 '24 14:07 hugovk

I'd suggest following PyData Sphinx Theme honestly -- Furo's approach works because of the weird stacking it has due to the legacy of being a light-only theme that evolved a dark mode and requires... shenanigans (see https://github.com/pradyunsg/furo/blob/main/src/furo/assets/styles/base/_theme.sass which includes dark colors twice).

PyData's approach allows content to only care about the current display theme, with keeping relevant "what mode is selected" state separately, which would work better for the usecases like showing the dark mode SVG when content is dark.

pradyunsg avatar Jul 22 '24 17:07 pradyunsg

"Hi, team! I’ve reviewed the discussion, and it seems using a data-attribute for tracking theme modes is a more robust solution. Can you provide a small code snippet or example for implementing dynamic dark mode functionality with data-color_scheme or similar attributes? This will help me ensure a consistent and standards-compliant integration in my contributions. Thank you!"

Here as the sample code:

<html lang="en" data-color_scheme="light">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Dark Mode Example</title>
  <style>
    body {
      font-family: Arial, sans-serif;
      transition: background-color 0.3s, color 0.3s;
    }
    [data-color_scheme="light"] body {
      background-color: #ffffff;
      color: #000000;
    }
    [data-color_scheme="dark"] body {
      background-color: #000000;
      color: #ffffff;
    }
  </style>
</head>
<body>
  <h1>Dark Mode Example</h1>
  <button onclick="toggleTheme()">Toggle Dark Mode</button>
  <script>
    function toggleTheme() {
      const html = document.documentElement;
      const currentTheme = html.getAttribute('data-color_scheme');
      const newTheme = currentTheme === 'light' ? 'dark' : 'light';
      html.setAttribute('data-color_scheme', newTheme);
    }
  </script>
</body>
</html>

ONKARBH avatar Jan 21 '25 10:01 ONKARBH

This account is definitely a bit suspicious🤔💀

Wulian233 avatar Jan 21 '25 11:01 Wulian233

@Wulian233 Sir, I am new to this field, and I am slowly learning all these technologies. I don’t have much knowledge about them, and there’s no one to guide me, especially like you do. Most importantly, I don’t have a strong command of the English language. I found your projects very approachable, and they are extremely helpful for beginners like me.

I didn’t know earlier how to push the changes made locally to the previous pull request, but I learned it under your guidance. You had assigned me work related to the sidebar issue, but I couldn’t complete it as per your expectations. However, I have now improved the sidebar in my local project.

Your guidance has been immensely helpful to beginners like me. I apologize for taking up your time.

ONKARBH avatar Jan 21 '25 13:01 ONKARBH

The root problem is that inline SVG is styled via prefers-color-scheme, so when a user forces dark with the theme selector (while the OS is in light), there’s no DOM signal for CSS to hook onto. The clean fix is to expose the current selection on the root element and let authors target it. Use a single data attribute on :

data-theme: "light" | "dark" | "auto"

When mode = auto, remove the attribute so system media queries keep working. This mirrors PyData’s approach: keep “what the user picked” (auto/light/dark) separate from “how CSS applies.” With this contract, content authors can reliably style SVG and anything else.

CSS (solves the SVG case without breaking auto)

/* Base (light) */
svg { background-color: transparent !important; }
line, polyline, polygon, rect { stroke: #000; fill: none; stroke-opacity: 1; }
polygon.filled { fill: #f00; }
text { fill: #000; stroke: none; font-family: sans-serif; font-style: normal; font-weight: normal; text-anchor: start; }

/* Forced dark via selector */
html[data-theme='dark'] line,
html[data-theme='dark'] polyline,
html[data-theme='dark'] polygon,
html[data-theme='dark'] rect { stroke: #fff; }
html[data-theme='dark'] text { fill: #fff; }

/* Auto = no attribute; defer to OS */
@media (prefers-color-scheme: dark) {
  html:not([data-theme]) line,
  html:not([data-theme]) polyline,
  html:not([data-theme]) polygon,
  html:not([data-theme]) rect { stroke: #fff; }
  html:not([data-theme]) text { fill: #fff; }
}

JS (tiny switcher contract the theme can call)

<script>
  // Apply user selection: 'light' | 'dark' | 'auto'
  function applyTheme(mode) {
    const html = document.documentElement;

    if (mode === 'auto') {
      html.removeAttribute('data-theme'); // let prefers-color-scheme rule
    } else {
      html.setAttribute('data-theme', mode); // force 'light' or 'dark'
    }
    try { localStorage.setItem('theme', mode); } catch (e) {}
  }

  // Initialize on load from previous selection (default to auto)
  document.addEventListener('DOMContentLoaded', () => {
    const saved = (function() {
      try { return localStorage.getItem('theme'); } catch (e) { return null; }
    })();
    applyTheme(saved || 'auto');
  });

  // Keep AUTO in sync with OS changes
  const mql = window.matchMedia('(prefers-color-scheme: dark)');
  mql.addEventListener('change', () => {
    const mode = (function() {
      try { return localStorage.getItem('theme') || 'auto'; } catch (e) { return 'auto'; }
    })();
    if (mode === 'auto') applyTheme('auto');
  });

  // Optional: expose a stable hook if the theme already has a switcher
  // window.activateTheme = applyTheme;
</script>

Optional no-FOUC pre-init (sets the attribute before first paint)

<script>
  (function() {
    try {
      var mode = localStorage.getItem('theme') || 'auto';
      if (mode !== 'auto') document.documentElement.setAttribute('data-theme', mode);
    } catch (e) {}
  })();
</script>

How this closes the issue: authors now have a deterministic selector (html[data-theme='dark']) for forced dark, and “auto” still honors prefers-color-scheme. Inline SVG and other embedded assets render correctly regardless of whether the user toggles via the site selector or the OS setting. Happy to open a PR wiring the attribute + storage and documenting the CSS contract above.

CodeKraken-cmd avatar Nov 02 '25 13:11 CodeKraken-cmd