Dark Mode in a single CSS theme file, and html.dark selector

Setup

  • A single, fully themed, 2 color mode, Bootstrap CSS
  • Driven by .dark & .light <html> tag classes
  • Requires JavaScript & jQuery
  • Not dependant on prefers-color-scheme, but honours it initially

Replace the bootstrap stylesheet with the following code:

<!-- Bootstrap CSS -->
<link rel="stylesheet" href="bootstrap-nightshade.css">

You will also need the following code to toggle a `dark` class in the <html> tag with this code:

<script>
  $(document).ready(function(){

    // This code uses `localStorage` to save "user prefers color" persistently
    // This key used is `user-prefers-color`, and should be one of:
    //  0 = only used at first trigger, to indicate firts time selection
    //  1 = user wants light mode
    //  2 = user wants dark mode
    // the key can also be deleted to indicate user has no preference.

    // function to toggle the css
    function toggle_color_scheme_css($mode) {
      // amend the body classes
      if ($mode == 'dark') {
        $("html").removeClass('light').addClass("dark");
      } else {
        $("html").removeClass("dark").addClass('light');
      }
      // if on user prefers color then update stored value
      $upc = window.localStorage.getItem('user-prefers-color');
      if ($upc !== null) {
        if ($upc == 0) $("#css-save-btn").prop( "checked", true );  // first time click
        window.localStorage.setItem('user-prefers-color', ($mode == 'dark') ? 2 : 1);
      }
    }

    // function / listener action to toggle the button
    function update_color_scheme_css() {
      $upc = window.localStorage.getItem('user-prefers-color');
      if (($upc === null) || ($upc == 0)) {
        $mode = (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) ? 'dark' : 'light';
      } else {
        $mode = ($upc == 2) ? 'dark' : 'light';
      }
      $("#css-toggle-btn").prop( "checked", ('dark' == $mode) );
      toggle_color_scheme_css($mode);
    }

    // initial mode discovery & update button
    update_color_scheme_css();
    if (window.localStorage.getItem('user-prefers-color') !== null)
      $("#css-save-btn").prop( "checked", true );

    // update every time it changes
    if (window.matchMedia) window.matchMedia("(prefers-color-scheme: dark)").addListener( update_color_scheme_css );

    // toggle button click code
    $("#css-toggle-btn").bind("click", function() {
      // disable further automatic toggles
      if (window.localStorage.getItem('user-prefers-color') === null)
        window.localStorage.setItem('user-prefers-color', 0);
      // get current mode, i.e. does body have the `.dark`` classname
      $mode = $("html").hasClass("dark") ? 'light' : 'dark';
      toggle_color_scheme_css($mode);
    });

    // toggle button click code
    $("#css-save-btn").bind("click", function() {
      $chk = $("#css-save-btn").prop("checked");
      if ($chk) {
        // user wants persistance, save current state
        $upc = $("html").hasClass("dark") ? 2 : 1;
        window.localStorage.setItem("user-prefers-color", $upc);
      } else {
        // user doesn't want pesistace, delete saved key
        window.localStorage.removeItem("user-prefers-color");
      }
    });

  });
</script>

This code features a persistance engine that uses JS' `window.localStorage` to "remember" the users option.

The issue with the above approach is the “content flash” that happens when the user has persisted a “dark” mode. On a reload, code will transition to dark as designed but there will be a sub-second flash as the light page loads first and then gets transitioned out. This is similar to FOUC and is well know to developers - with few elegant fixes.

Read the README.md for more on this proof of concept.