Skip to content

Instantly share code, notes, and snippets.

@loilo
Last active March 28, 2026 12:25
Show Gist options
  • Select an option

  • Save loilo/dd5639089d837e95c22a706260b26706 to your computer and use it in GitHub Desktop.

Select an option

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;
}
}
@wonsuc

wonsuc commented Mar 26, 2024

Copy link
Copy Markdown

Thank you for detail explanation.
I asked this because it didn't work with ::before, ::after pseudos.

For examples,

.foo::before {
  @include dark {
    opacity: 0.5%;
  }
}

I'm looking for an approach which can solve this problem.

@loilo

loilo commented Mar 26, 2024

Copy link
Copy Markdown
Author

@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

wonsuc commented Mar 27, 2024

Copy link
Copy Markdown

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