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

Closed
opened 2026-02-05 02:22:12 +03:00 by OVERLORD · 10 comments
Owner

Originally created by @Baptistou on GitHub (Dec 12, 2020).

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 :

  • light --> use light mode
  • dark --> use dark mode
  • auto --> mode based on prefers-color-scheme, set by default

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

Originally created by @Baptistou on GitHub (Dec 12, 2020). **Describe the feature you'd like** Set light/dark mode by default according to [prefers-color-scheme](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme). Moreover, if an .env variable is created (#2081), provide 3 values : - ```light``` --> use light mode - ```dark``` --> use dark mode - ```auto``` --> mode based on prefers-color-scheme, set by default **Describe the benefits this feature would bring to BookStack users** Improve UX in the same way as language auto detection from user agent.
Author
Owner

@ssddanbrown commented on GitHub (Dec 12, 2020):

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.

@ssddanbrown commented on GitHub (Dec 12, 2020): 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.
Author
Owner

@Baptistou commented on GitHub (Dec 13, 2020):

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
}
@Baptistou commented on GitHub (Dec 13, 2020): 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 : ```html <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> ``` ```css @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 } ```
Author
Owner

@ssddanbrown commented on GitHub (Dec 13, 2020):

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.

@ssddanbrown commented on GitHub (Dec 13, 2020): > 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.
Author
Owner

@Baptistou commented on GitHub (Dec 13, 2020):

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.

@Baptistou commented on GitHub (Dec 13, 2020): > 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](https://developer.mozilla.org/en-US/docs/Web/API/Document/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.
Author
Owner

@ssddanbrown commented on GitHub (Dec 13, 2020):

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.

@ssddanbrown commented on GitHub (Dec 13, 2020): > 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.
Author
Owner

@geins commented on GitHub (Apr 26, 2021):

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>
@geins commented on GitHub (Apr 26, 2021): 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> ```
Author
Owner

@Write commented on GitHub (Jan 20, 2024):

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>
@Write commented on GitHub (Jan 20, 2024): 2024 edition : ```javascript <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> ```
Author
Owner

@jasonpincin commented on GitHub (Aug 29, 2024):

Thanks @Write ! FWIW - I'd love to see three options for color scheme in Bookstack: Light, Dark, and Auto. Default to auto. Seems like it'd solve some of the complexities described? Maybe? Some users like the mode to changed based on time of day. Others like to pin it one way or the other.

Edit to say: other apps I've used employ the term "system" instead of "auto". To imply the mode matches your system preference.

@jasonpincin commented on GitHub (Aug 29, 2024): Thanks @Write ! FWIW - I'd love to see three options for color scheme in Bookstack: Light, Dark, and Auto. Default to auto. Seems like it'd solve some of the complexities described? Maybe? Some users like the mode to changed based on time of day. Others like to pin it one way or the other. Edit to say: other apps I've used employ the term "system" instead of "auto". To imply the mode matches your system preference.
Author
Owner

@Kali187 commented on GitHub (Jul 15, 2025):

Thanks @Write ! FWIW - I'd love to see three options for color scheme in Bookstack: Light, Dark, and Auto. Default to auto. Seems like it'd solve some of the complexities described? Maybe? Some users like the mode to changed based on time of day. Others like to pin it one way or the other.

It is a good practice.
User can specifically fix the mode per app, but default should be auto (system). In this case I can just use my system settings to toggle all apps automatically, or like you said, adapt to time of the day automatically.

@Kali187 commented on GitHub (Jul 15, 2025): > Thanks [@Write](https://github.com/Write) ! FWIW - I'd love to see three options for color scheme in Bookstack: Light, Dark, and Auto. Default to auto. Seems like it'd solve some of the complexities described? Maybe? Some users like the mode to changed based on time of day. Others like to pin it one way or the other. It is a good practice. User can specifically fix the mode per app, but default should be auto (system). In this case I can just use my system settings to toggle all apps automatically, or like you said, adapt to time of the day automatically.
Author
Owner

@hepdci commented on GitHub (Oct 4, 2025):

Here’s an alternative solution: If no initialization has been done, the OS is in dark mode, and BookStack is not yet in dark mode, then simulate a click on the dark mode button. This way, both the light and dark mode buttons remain available.

<script>
document.addEventListener('DOMContentLoaded', function () {
  const toggleForm = document.querySelector('form[action*="/preferences/toggle-dark-mode"]');
  const root = document.documentElement;

  if (toggleForm) {
    const alreadyDark = root.classList.contains('dark-mode');
    const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;

    if (!alreadyDark && prefersDark && !localStorage.getItem('bs_theme_initialized')) {
      localStorage.setItem('bs_theme_initialized', '1');
      toggleForm.submit();
    }
  }
});
</script>
@hepdci commented on GitHub (Oct 4, 2025): Here’s an alternative solution: If no initialization has been done, the OS is in dark mode, and BookStack is not yet in dark mode, then simulate a click on the dark mode button. This way, both the light and dark mode buttons remain available. ```html <script> document.addEventListener('DOMContentLoaded', function () { const toggleForm = document.querySelector('form[action*="/preferences/toggle-dark-mode"]'); const root = document.documentElement; if (toggleForm) { const alreadyDark = root.classList.contains('dark-mode'); const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; if (!alreadyDark && prefersDark && !localStorage.getItem('bs_theme_initialized')) { localStorage.setItem('bs_theme_initialized', '1'); toggleForm.submit(); } } }); </script> ```
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: starred/BookStack#1972