csslogicmedia-queriesthemescss-variables

Site color theming with CSS Custom Properties and @media


The popular direction in allowing for light/dark theme switching is by using CSS Custom Properties. It is most common to simply create a toggle on the page to select light or dark. I'm interested in adding a layer of complexity that allows the user to select the system preference OR an override (of light or dark). Selecting the system preference would then read light and dark at the @media level. Unless the user hasn't selected a color-scheme, in which case we'll default to some scheme (let's say light).

Assuming the overrides are set by applying attributes to document.documentElement.

The challenge here is that I want to see if I can write the variables for the light/dark themes once as it seems every solution I can think of requires a set of variables to be written in two rulesets.

Looking for a native CSS solution here, as I could do this with build tools; SASS @mixin code below.

@mixin dark-theme() {
  --bg-color: black;
}

:root {
  --bg-color: white;
}

html[theme="dark"] {
  @include dark-theme(); /* 1st occurrence */
}

@media (prefers-color-scheme: dark) {
  html:not([theme="light"]) {
    @include dark-theme(); /* 2nd occurrence */
  }
}

body {
  background-color: var(--bg-color);
}

I'm expecting some sort of combination using :not([theme]) and @media not all and (prefers-color-scheme) but I've tried a few combinations and have gotten close but not 100% of the logic above. I've also tried switching the specific theme for the fallback (setting the dark theme at the :root and then attempting to generalize in the @media rules to apply the light theme) but that didn't seem to work either.

Ah, no JavaScript solutions either please. Again, looking to see if native CSS is possible.


Solution

  • Coming back four years later now that CSS has introduced light-dark() we can define both light and dark once within the new function and then change the application of those variables with color-scheme.

    body {
      color-scheme: light;
    }
    
    body[data-theme="dark"] {
      color-scheme: dark; /* 1st occurrence */
    }
    
    @media (prefers-color-scheme: dark) {
      body:not([data-theme="light"]) {
        color-scheme: dark; /* 2nd occurrence */
      }
    }
    
    body {
      background-color: light-dark(white, black);
    }
    

    I've opted to use body since the content presentation is what we are affecting and updated to use data attributes to trigger instead of non-standard ones.