home ~ projects ~ socials

Duplicate CSS Rules With JavaScript

I just finished making a Vanilla JavaScript Color Scheme Switcher. It lets you choose from between light mode, dark mode, and "system". The last one looks for data passed in from the operating system to determine if dark or light mode should be used. The CSS for the approach I'm using requires duplicating the rules for the dark mode1.

I don't have any CSS processing set up for my site. I'd have to copy/paste the values every time I made a change. That's both a pain and super prone to error. Instead of going that route I figured I'd try to use JavaScript to handle the duplicating for me.

This is what I came up with:

/scripts/theme-support.js

function duplicateDarkStyles() {
  for (let sheetNum = 0; sheetNum < document.styleSheets.length; sheetNum++) {
    const sheet = document.styleSheets[sheetNum]
    for (let ruleNum = 0; ruleNum < sheet.cssRules.length; ruleNum++) {
      const rule = sheet.cssRules[ruleNum]
      if (rule.conditionText === "(prefers-color-scheme: dark)") {
        for (let subNum = 0; subNum < rule.cssRules.length; subNum++) {
          const subRule = rule.cssRules[subNum]
          if (subRule.selectorText === ":root") {
            const ruleString = subRule
            const parsedString = ruleString.cssText.replace(subRule.selectorText, "")
            sheet.insertRule(`[data-scheme="dark"] ${parsedString}`, sheet.cssRules.length)
          }
        }
      }
    }
  }
}

document.addEventListener("DOMContentLoaded", () => {
  duplicateDarkStyles()
})

I've done initial testing in all my Mac browsers and things seem to be working fine. There are a couple keys to the process:

  1. The JavaScript needs to be in an external file (in this case called /scripts/theme-support.js)
  2. The script needs to be called be called from the of the page with the defer attribute set. For example:

    <script src="/scripts/theme-support.js" defer></script>

The reason comes from the MDN docs2.

DOMContentLoaded does not wait for stylesheets to load, however deferred scripts do wait for stylesheets, and the DOMContentLoaded event is queued after deferred scripts.

Without waiting the script could easily miss making the updates if the CSS file hasn't loaded yet. As near as I can tell from the docs that should happen when calling the external script with defer.

-- end of line --

Endnotes

  • My script specificallly looks for the media query (prefers-color-scheme: dark) and copies the contents of it's :root into a new rule for [data-scheme="dark"]. That's done using conditionText which works for media queries. The API for accessing other CSS rules is different.

References

Footnotes

1 ⤴

The explicit "dark" mode uses a [data-theme="dark"] matcher while the system version uses @media (prefers-color-scheme: dark) and those two things can't currently be combined.

2 ⤴