Repository navigation
Set CSS value in HTML to reflect current theme #192
Description
Activity
- changed the title
[-]SVG created to work in dark and light mode works as expected when using the browser to set the mode, but not when using the theme selector[/-][+]Set CSS value in HTML to reflect current theme[/+]on Jul 21, 2024 Yeah, https://peps.python.org/ has a
data-colour_schemeattribute on thehtmlelement that changes betweendark,lightandautobased on the theme selection.I'd welcome a PR to do something like this here too.
Do you want it to match the
peps.python.orgbehaviour exactly? Or is what I've implemented (using thebodytag, and the attribute has a-themesuffix) OK?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-modethat toggles the thee states.Hmm, shall we follow Furo?
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.
Reacted by Hugo van KemenadeThe 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.
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 theactivateTheme()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. adark-themeclass 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: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-themeclass to thebodyor 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?