Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Override system light/dark mode per user profile #1033

Merged
merged 2 commits into from
Dec 1, 2021

Conversation

DanielHeath
Copy link
Contributor

Fairly simplistic approach - moved the css variables into a .css.erb file and wrote the dark mode settings out twice (once for system, once for the override.

You could avoid sending 20 lines of CSS twice with some JS, but that seems like substantially more overhead.

@arp242
Copy link
Contributor

arp242 commented Oct 7, 2021

I wonder if it makes sense to display this in the footer instead, and store this setting as a cookie or localStorage instead of in the database?

I believe there are quite a few people who only read Lobsters and don't have an account – as is common for a lot of discussion forums – but they won't be able to change the colour scheme. And due to the invite system you can't really tell them to "just create an account" either.

This will also allow people to set preferences per-device; e.g. light on desktop and dark on mobile.

I don't even think it's all that much work?

@DanielHeath
Copy link
Contributor Author

Yeah, per-session would also be pretty easy to do.

@belak
Copy link

belak commented Oct 7, 2021

If you decide to go the JS route, anyone is welcome to use or derive from the dark-mode code I have on my blog for Lobsters (or any other project). I've been meaning to write a small post about it, but it hasn't been a priority lately.

// systemDarkMode returns true if system-wide dark mode is enabled.
function systemDarkMode() {
  return window.matchMedia('(prefers-color-scheme: dark)').matches;
}

// getDarkMode returns true if an override value is in localStorage or
// system-wide dark mode is enabled.
function getDarkMode() {
  const storedValue = localStorage.getItem('darkMode');
  if (storedValue !== undefined) {
    return storedValue !== 'false';
  }

  return systemDarkMode();
}

// toggleDarkMode toggles if dark mode is enabled and saves it in local storage.
function toggleDarkMode() {
  const darkMode = getDarkMode();
  localStorage.setItem(
    'darkMode',
    darkMode ? 'false' : 'true',
  );

  updateDarkMode();
}

// updateDarkMode sets the proper classes on the body based on the current state
function updateDarkMode() {
  if (getDarkMode()) {
    document.body.classList.remove('light-mode');
    document.body.classList.add('dark-mode');
  } else {
    document.body.classList.add('light-mode');
    document.body.classList.remove('dark-mode');
  }

  // We need a way to show/hide the reset button
  if (!localStorage.getItem('darkMode')) {
    document.body.classList.remove('dark-mode-value-set');
  } else {
    document.body.classList.add('dark-mode-value-set');
  }
}

// clearDarkMode removes the set value from localStorage and falls back to
// whatever the OS says.
function clearDarkMode() {
  localStorage.removeItem('darkMode');
  updateDarkMode();
}

// Ensure that if the value changes in another tab, all of them will update.
window.addEventListener('storage', updateDarkMode);
updateDarkMode();

let element = document.getElementById('dark-mode-toggle');
element.style.display = 'block';

The downside of going that route is that for people who want it to sync across all devices, it won't because there's nothing saved server side. This works great for a static blog, it might not be quite as convenient for a site with accounts. It also means that people who have JS disabled will not be able to change the override, though I'd imagine that's a relatively small group of people.

@pushcx
Copy link
Member

pushcx commented Oct 7, 2021

We don't serve any js unless people log in, and I don't want to add a lot of complexity to this for people who don't want to configure their browser.

@arp242
Copy link
Contributor

arp242 commented Oct 7, 2021

The thing is that browsers don't really allow selecting this per-website AFAIK, or at least not easily.

But I don't really care much myself; it's just a thought.

@Sembiance
Copy link

You can accomplish a dark/light theme 'toggle' with pure CSS, no javascript, using the 'Checkbox Hack':
https://css-tricks.com/the-checkbox-hack/
https://www.sitepoint.com/building-style-switcher-with-pure-css-using-checked/

Essentially you put a hidden checkbox at top level:

<body><input type="checkbox" id="darkModeCheckbox" style="display: none;"></body>

Somewhere else, at the same depth top level, you can put text or icons under <label> that will control the checkbox:

<label for="darkModeCheckbox" id="darkModeDarkLabel">switch to dark</label>
<label for="darkModeCheckbox" id="darkModeLightLabel">switch to light</label>

Then you only show one of the labels depending on whether it's currently checked or not:

#darkModeLightLabel { display: none; }
#darkModeCheckbox:checked ~ #darkModeLightLabel { display: inline; }
#darkModeCheckbox:checked ~ #darkModeDarkLabel { display: none; }

So by default, the checkbox is unchecked and the words switch to dark will appear on the website.
Clicking that label will toggle the checkbox, which will then hide the switch to dark label and instead show the switch to light label.

You can change the labe HTML to whatever you want, two images, maybe a light bulb lit/unlit or any other 'toggle' type icon you want to use.

Now that you can toggle this hidden checkbox on and off, you can now style the site based on whether the checkbox is checked.

#darkModeCheckbox:checked ~ any other css here { whatever: "you want"; }

Problem is, there is no 'stickyness' of this setting without JavaScript. So every time you visited a different page, it would revert back to it's default. Also it won't auto detect based on dark mode preferences either, since it's all gated behind the checkbox hack.

The only way around that would be introducing a tiny little javascript snippet to persist the checkbox state and auto set that state based on the persisted setting, falling back to setting it based on detected CSS prefs (which CAN be done with JS).

@kconner
Copy link
Contributor

kconner commented Oct 7, 2021

Hi, I did #1026. My wish list would be:

  • Keep using the OS theme setting via the media query as default behavior
  • Let users override that to always light or always dark, or reset to automatic
  • Persist that decision across page views and site visits

A three-option setting defaulting to automatic and stored in a cookie would fit the bill.

<%= light %>
}

@media (prefers-color-scheme: dark) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm rusty on media queries. Is it be possible to do something like:

@media (prefers-color-scheme: dark), html.color-scheme-dark {

So that we don't have to move these into variables and repeat them?

Copy link
Contributor Author

@DanielHeath DanielHeath Oct 11, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately not; media queries and selectors do not exist at the same level & cannot be combined.

I could add sass to the sprockets pipeline, which would let us write something sensible and have it compiled into valid CSS - but I didn't want to add a dependency without consulting first, and there was already a .css.erb files (indicating to me a preference for doing it that way)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's unfortunate. I'd rather not add sass, correct. The asset pipeline is unreliable enough.

@pushcx
Copy link
Member

pushcx commented Oct 7, 2021

I left a comment noting where we may be able to simplify this. Could you add tests that unset produces the -system css class (or no class), and setting light or dark sets those css classes?

<html lang="en">
<html
lang="en"
class='color-scheme-<%= @user&.prefers_color_scheme || 'system' %>'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is almost at the amount of complexity where I'd want to stick it into a helper. If it gets any more complexity, please do so.

@DanielHeath
Copy link
Contributor Author

Anything still outstanding on this PR?

@pushcx
Copy link
Member

pushcx commented Nov 17, 2021

I'm trying to sucker @kconner into taking a look to see if they can spot a way out of repeating the CSS, but more likely I'm just working up to biting the bullet on that.

@kconner
Copy link
Contributor

kconner commented Nov 17, 2021

@pushcx Sorry about letting your email get repeatedly buried. I don't see an obvious way to DRY this, but my working knowledge of CSS 3 is largely happenstance, and there might be a way.

@pushcx
Copy link
Member

pushcx commented Nov 17, 2021

Thank you for letting me sucker you into taking a look. I will step up my work on accepting the inevitable.

@pushcx
Copy link
Member

pushcx commented Dec 1, 2021

I have accepted that repetition is the only way to implement this, given the constraints. Thank you again for contributing this, and for your patience with my emotional process.

I spent a while tinkering. There's really no way for the server to change something in the HTML that a media query can pick up, CSS can't express an or between prefers-color-scheme and selector, and javascript is ugh.

@pushcx pushcx merged commit 083cd38 into lobsters:master Dec 1, 2021
@RaymiiOrg
Copy link

I know this is not techsupport, but after disabling the setting (from system to light), the front-page hasn't updated entirely:

Screenshot_20211202-075322174

Comment pages are fine, except for the hats symbol is also off:

Screenshot_20211202-075423067

A logout gives me the dark site, logging back in gives me the screenshot above. (Does not help).

I'm on Firefox on Android (11), but I do have battery saving (doze) mode on, which enables the dark mode system wide. Can't turn off just dark mode and still have battery saving...

@pushcx
Copy link
Member

pushcx commented Dec 2, 2021

No apology needed, this is the best place to report an issue with this PR.

@DanielHeath Can you take a look? At a glance I'd guess there's a color or two that didn't survive getting moved?

@DanielHeath
Copy link
Contributor Author

Looking. Apparently the small-screen color scheme was defined separately and uses hard-coded colors.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants