Sun Moon Toggle with Hooks

Building dark mode with hooks and blending mode.


In this talk, I'd like to speak with you how I created dark mode for my own site using hooks, and how I came to realization that with the tools we have on hand, it is possible to implement a generalized dark mode for any site, without having to come up with a load of CSS colors at all.

To implement a dark mode to a site, you need two things. First, you need a toggle to control dark mode and light mode. And second, you need to implement the actual styles for the dark mode.

The Hook

The toggle here is the hooks part. And in regards of styling, for those of you who missed my talk on mix blend mode, here is your second chance learning how to do this hipster dark mode to your site without writing any extra colors in your CSS.

I lied to you a little bit by saying I implemented this thing with hooks + and the UI. Because, before this talk, I already implemented the dark mode in my site using internal state. Let me first share with you what the code looked like before hooks.

My site has a Layout component that wraps around everything. It used to be a functional component, not anymore when I had to add an internal state for toggle.

The UI for the toggle also lives inside Layout. It implements a onClick handler which will setState for the toggle. It will also set the theme variable in local storage so that you can leave the site and come back with your preferred theme, this is also what we consider as a "side effect".

Use hooks for the theme toggle

Here is the hook.

I call useState with an initial state, which computes to whether I see a local storage or not, if not, I use the default light theme.

The useState call returns an array which destructs to, one, a state variable, and two, a set state handler which I use to write my own toggleTheme handler.

Previously I write to local storage within the setState function. Since writing local storage is a "side effect" if you tend to be functional, we can use the useEffect hook to write to local storage whenever theme changes. This is controlled by passing in the array with variable theme in to the second parameter to useEffect.

const [theme, setTheme] = useState(
  typeof window !== 'undefined'
    ? window.localStorage.getItem('theme')
    : THEMES[0]
)

const toggleTheme = currentTheme => {
  const newTheme = getNewTheme(currentTheme)
  setTheme(newTheme)
}

useEffect(() => {
	typeof window !== 'undefined' &&
    window.localStorage.setItem('theme', theme)
}, [theme]);

Refactor the toggle to its own component

Next, I refactored these two pieces into its own component.

As you can see in the changes here, which is nothing more than a move around to make the Layout component a lot cleaner.

This is not yet the part that is special to hooks yet. Because we can move everything into its own component with React's class component as well.

Refactor to custom hook

This is one feature of hooks that people are excited about.

With this refactor, we take away the logic part of the toggle into a separate file, commonly referred to as a "custom hook". I can let my custom hook return only the pieces of information it needs to share with the UI components that use it.

In this case, we return the theme variable, and the custom toggleTheme handler that we pass to divs as onClick event.

Now that the toggle file is pretty clean. It no longer has the cryptic blocks for the hooks. And so we're left with this little block of code. I'm not sure about you but I then immediately noticed that this file is still coupled with the logic of the theme. As you may see here the class names have ternary logic that is based on whether the theme matches certain theme or not.

This wasn't so obvious earlier on when everybody lived under Layout. Now since we moved the logic out thanks to hooks, we realize that this part of the UI is still aware of the logic. Is this optimal? Not so sure. But it seems to me now that it is not too hard to optimize a little bit? I realize that the first line adds a class for the dark mode, and the second one adds a theme for the light mode. So I refactored these again (use github compare).

Now the UI is left with only two class names describing what they are, plus two more class names that are simply the themes.

And in the CSS file? Previously the class names were semantic to what it does in terms of styling. And it's a bit hard to remember what's expanding? In what case got z index? Now the CSS says, here is your blender (we'll talk about what it is later), and here's what it looks like under the moon. And here's the toggle, and here's what it looks like under the sun.

Recap

  • Toggle coupled with Layout with internal state
  • Move UI to separate component with hook
  • Move (again) logic to a separate file
  • Cleaner cut between logic and UI

Let's take a look at what we've gone through.

First we had a Layout that had a toggle in it, and we thought it was great cuz where else should the toggle live in?

Then we moved the toggle into its own file alongside with its hook. Our Layout returns to a "dummy", functional component again, with a little toggle with a hook attached to it.

Then, we extracted the hooks logic out again and we realize there is a more declarative way of writing our UI as well as our CSS.

The UI

I'd like to pause here on hooks a little bit and look at the business feature we're implementing, the dark mode.

You've already seen all the code. There is no extra colors involved. Why?

This goes back to the blending mode I talked about last month. Later on I gave another talk at SingaporeCSS with less math and more nonsense in it, tweet. Most of us weren't there.

The idea is that there is a CSS property called mix-blend-mode that will ask you for a way to blend colors when graphics stack on each other. And then there is one such blending mode called difference that will blend the colors by taking their difference.

In CSS's standard RGB color space, white is all the light, black is zero which is synonymous to "nothing".

diff(white, white) = white - white = 0 = black

diff(white, black) = white - black = white - 0 = white

By taking the difference of my whole site against a full-screen white-colored div, the background that used to be white becomes black, and the foreground that used to be black becomes white.

In fact, you don't have to use white as the color to compute the dark mode — in some case you shouldn't. If you take the difference of your site against the color of your background, you will preserve the original contrast between your foreground and background. And if your original background is rather light, you'll get a natural dark mode everything computed automatically.

Having this in mind, I basically just started doing this to nearly every website I visit. It's a cannot unsee situation.

So now we potentially have a magic component that can theoretically turn every website into dark mode under the moon. Basically, all we need is its background color. You can do some kind of AST traversing and get the site's background color, but how sneaky is that! We can just ask :D We're dealing with a React component and we can use props :)

<SunMoonToggle backgroundColor="#efefef" type="hipster" />

So, I moved the component to a package by itself and published it. Honestly, the building step doesn't quite work at the moment, which I'll fix over this weekend lol. But that is the inspiration brought in by hooks that I'd like to share with everyone.

Separating UI and logic helps us write more readable, declarative code.