Skip to content

Instantly share code, notes, and snippets.

@loilo
Last active April 9, 2024 20:01
Show Gist options
  • Star 29 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save loilo/dd5639089d837e95c22a706260b26706 to your computer and use it in GitHub Desktop.
Save loilo/dd5639089d837e95c22a706260b26706 to your computer and use it in GitHub Desktop.
Sass Dark/Light Theme Mixin

Sass Dark/Light Theme Mixin

This is a Sass mixin to handle a 3-way dark mode. It relies on a data-theme attribute on your <html> element with a value of light or dark. If data-theme is absent (i.e. it's neither light nor dark), the system's preferred mode is used.

body {
  // matches data-theme="light" or data-theme="auto" with system instructing light mode
  @include light {
    background: white;
    color: black;
  }
  
  // matches data-theme="dark" or data-theme="auto" with system instructing dark mode
  @include dark {
    background: black;
    color: white;
  }
}

See the example above on sassed

Caveat: This mixin targets modern browsers which support the :where() pseudo selector. There is an older revision of this Gist with more caveats, but also with support for older browsers, which you can find here.

@use 'sass:selector';
@use 'sass:string';
@use 'sass:list';
@mixin mode($mode) {
$opposite-mode: '';
@if $mode == 'light' {
$opposite-mode: 'dark';
} @else {
$opposite-mode: 'light';
}
$individual-selectors: selector.parse(&);
$new-selectors: ();
// Split into individual selectors
@each $individual-selector in $individual-selectors {
// Split by :: to find pseudo elements
$parts: string.split(#{$individual-selector}, '::', 2);
@if list.length($parts) > 1 {
// Selector has pseudo elements:
// Split up the selector, apply mode modifier and stick the pseudo element on again
$new-selectors: list.append(
$new-selectors,
"#{selector.append(list.nth($parts, 1), ':where(:root:not([data-theme=#{$mode}]):not([data-theme=#{$opposite-mode}]) *)')}::#{list.nth($parts, 2)}",
comma
);
} @else {
// Selector has no pseudo elements: Simply append the :where() clause
$new-selectors: list.append(
$new-selectors,
selector.append($individual-selector, ':where(:root:not([data-theme=#{$mode}]):not([data-theme=#{$opposite-mode}]) *)'),
comma
);
}
}
@media (prefers-color-scheme: $mode) {
@at-root #{$new-selectors} {
@content;
}
}
@at-root #{$new-selectors} {
@content;
}
}
@mixin light {
@include mode('light') {
@content;
}
}
@mixin dark {
@include mode('dark') {
@content;
}
}
@loilo
Copy link
Author

loilo commented Mar 26, 2024

@wonsuc You're right. I was aware of that caveat, but didn't add it to the readme. Your case inspired me to adjust the mixins. They're slightly more complex now, but they also support pseudo elements like ::before now.

@wonsuc
Copy link

wonsuc commented Mar 27, 2024

Thank you for your efforts. Works good :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment