BookStackApp / BookStack

A platform to create documentation/wiki content built with PHP & Laravel
https://www.bookstackapp.com/
MIT License
14.67k stars 1.85k forks source link

Use prefers-color-scheme to set light/dark mode by default #2407

Closed Baptistou closed 3 years ago

Baptistou commented 3 years ago

Describe the feature you'd like Set light/dark mode by default according to prefers-color-scheme. Moreover, if an .env variable is created (#2081), provide 3 values :

Describe the benefits this feature would bring to BookStack users Improve UX in the same way as language auto detection from user agent.

ssddanbrown commented 3 years ago

Thanks for the suggestion @Baptistou.

I thought about using prefers-color-scheme when implementing dark mode but decided against it. prefers-color-scheme is primarily built for use in CSS media queries. I'd want to provide the user an option to control the preference on BookStack, non-dependant on browser/OS setting, so straight away that means we can't use the media query directly. We could detect the prefers-color-scheme state with JavaScript and set the controlling parent class depending on value but now we're adding mixed front-end and back-end logic for this control which means things are getting more complex. Upon that we would have to provide some level of 'auto' control as suggested so now we're needing some more complex user-control than just the toggle-button(s) we have now.

To me I didn't deem the use of the prefers-color-scheme worth implementing. If the option was provided by the browser as a header, as the language is, it'd be a bit more open to it.

Baptistou commented 3 years ago

And what about delegating the light/dark mode implementation to client side ? I see in the code that the mode button is sending a POST request to server /settings/users/toggle-dark-mode. This is a cost that could be avoided by handling the mode change directly in client side. You even don't need JavaScript at all, the following is a working example with pure CSS :

<body>
    <input type="radio" id="light-mode" name="mode">
    <label for="light-mode">Light Mode</label>
    <input type="radio" id="dark-mode" name="mode">
    <label for="dark-mode">Dark Mode</label>
    <main>
        <p>Hello World !</p>
    </main>
</body>
@media (prefers-color-scheme: light){
    #light-mode:indeterminate, #light-mode:indeterminate+label {
        display: none
    }
    main {
        color: #444;
        background-color: #F2F2F2;
    }
    #dark-mode:checked~main {
        color: #AAA;
        background-color: #111;
    }
}

@media (prefers-color-scheme: dark){
    #dark-mode:indeterminate, #dark-mode:indeterminate+label {
        display: none
    }
    main {
        color: #AAA;
        background-color: #111;
    }
    #light-mode:checked~main {
        color: #444;
        background-color: #F2F2F2;
    }
}

#light-mode:checked, #light-mode:checked+label,
#dark-mode:checked, #dark-mode:checked+label {
    display: none
}
ssddanbrown commented 3 years ago

You even don't need JavaScript at all, the following is a working example with pure CSS

Sure, But you're already having to do hacky workarounds, duplicating styles and abusing the intention of the media query. Additionally you'd still need to store the preference which, yeah, could be done with JS localStorage but that won't be cross-device.

Currently we control dark mode via a single CSS class on the html. The easiest way to achieve auto based on prefers-color-scheme would be to have some JavaScript to query the CSS media state than add that class. As said above though, we'd need to re-think the controls to be, what I think would be, less intuitive. Again, I just didn't see prefers-color-scheme being something worth implementing. Especially as, In my opinion, the browser implementation of that preference is half-baked.

Baptistou commented 3 years ago

Sure, But you're already having to do hacky workarounds, duplicating styles and abusing the intention of the media query.

Yes sure, JavaScript is the easiest way.

Additionally you'd still need to store the preference which, yeah, could be done with JS localStorage but that won't be cross-device.

Maybe we could use Cookie via JS to save the preference. This has the benefit to work cross-device and on public access. Having to reset the mode each time you arrive at home page or when you log out is quite annoying.

Again, I just didn't see prefers-color-scheme being something worth implementing.

Of course, this is not a big deal, this feature is not a priority.

ssddanbrown commented 3 years ago

Maybe we could use Cookie via JS to save the preference. This has the benefit to work cross-device and on public access.

Cookies themselves would not be a cross-device solution unless the browser happens to sync them.

Of course, this is not a big deal, this feature is not a priority.

Cool, I'll therefore going to close this off due to the reasons provided in my previous messages.

geins commented 3 years ago

For those interested in a little dirty tinkering:

It adds automatic dark-mode capability in most browsers. BUT the manual selection of the user doesn't work (switches will be removed) and it doesn't work on the settings page itself.

Add this to the custom header: (Don't forget to change https://YOURDOMAIN.XYZ to your actual domain)

<script>
// ** automatic dark-mode
  var root = document.getElementsByTagName( 'html' )[0]; //Get (first) html element
  //on page-ready
  document.addEventListener('DOMContentLoaded', function() {
    //switch to mode on page-load
    if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
      root.classList.add('dark-mode');
    } else {
      root.classList.remove('dark-mode');
    }

    //hide theme-selector switches
    //Drop-Down menu
    var drop_menu = document.getElementsByClassName('dropdown-menu')[0];
    drop_menu.removeChild(drop_menu.lastElementChild); //mode selector
    drop_menu.removeChild(drop_menu.lastChild.previousElementSibling); //hr

    //side-menu
    var side_menu = document.querySelector('form[action="https://YOURDOMAIN.XYZ/settings/users/toggle-dark-mode"]');     //mode selector
    if (side_menu !== null) {
      side_menu.remove(); 
    }
  }, false);

  //on mode-switch event from OS
  window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
      if (e.matches) {
        root.classList.add('dark-mode');
      } else {
        root.classList.remove('dark-mode');
      }
  });
</script>
Write commented 7 months ago

2024 edition :

<script>
  // ** automatic dark-mode
  var root = document.querySelector('html');

  document.addEventListener('DOMContentLoaded', function() {
    //switch to mode on page-load
    if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
      root.classList.add('dark-mode');
    } else {
      root.classList.remove('dark-mode');
    }

    // Hide theme-selector switches

    // Drop-Down menu
    var drop_menu = document.getElementsByClassName('dropdown-menu')[0];
    drop_menu.removeChild(drop_menu.lastElementChild.previousElementSibling); //mode selector
    drop_menu.removeChild(drop_menu.lastElementChild.previousElementSibling); //

    // Top bar
    document.querySelectorAll('form[action*=\\/preferences\\/toggle-dark-mode]').forEach(e => e.setAttribute("style", "visibility: hidden !important;"))
  }, false);

  // On mode-switch event from OS
  window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
      if (e.matches) {
         root.classList.add('dark-mode');
      } else {
         root.classList.remove('dark-mode');
      }
  });

</script>