Implementing Dark Mode in GatsbyJS with Styled Components and CSS Variables

in Programming & Dev4 years ago (edited)

darkmodegatsby.gif

Recently I had to implement dark mode for a website I was building with GatsbyJS and styled components, and it turned out not to be as straight forward as I thought it would, at least to get the smooth results I wanted and some cool features.

Features for this DarkMode:

  • Persist theme on localStorage so users always get to see their previously selected theme.
  • Use the prefererd color theme from the user's operating system through the prefers-color-scheme CSS media feature.
  • Don't show the wrong colors for an instant when loading the page (common issue with pre-generated HTML like gatsby, Next, etc).
  • Use styled components.

Since the focus of this post is to explain the implementation of the features mentioned above, for this example I'll be building a simple website with just enough elements to showcase the functionality.

You can check the Live Demo Here and the complete source code on my github.

What we'll be using:

  • React Context
  • React Hooks (UseEffect, UseMemo, UseState, UseContext)
  • CSS variables
  • Some server side rendering APIs from Gatsby
  • Some cool "tricks" for optimization

First lets create a gatsby project with gatsby-starter-default as boilerplate:

gatsby new dark-mode-styled https://github.com/gatsbyjs/gatsby-starter-default

Now install styled components and its plugin for gatsby:

npm install --save gatsby-plugin-styled-components styled-components babel-plugin-styled-components

The structure for this project is really simple since it's just a few components. I just added the constants directory where we'll store the values for the theme, and some components in the components folder. Apart from that the structure is the same as the starter bolierplate.

image.png

In the theme.js file we will be storing the color's values for the theme. This values will toggle the actual values of the css variables in our styles.

image.png

Let's start with the functionality

First we need a function to get the initial theme mode, it checks the preferred color scheme query and local storage to see if the user has used the toggle previsouly.

image.png

We'll make use of some state to store this value and React Context to manage it, so it's easily available in the other components.

image.png

In the context provider whose value we'll be using later in the toggle component, the setThemeMode function receives a parameter (newValue) to save the theme mode in local storage. The state and setter are passed as a value in the Context Provider so we can use them later in the toggle component.

The Toggle component

Every component below a context provider has access to it's context value, so we just have to import it and we get access to themeMode and setThemeMode from ThemeContext.

This component is just a checkbox whose checked attribute is conditioned by the themeMode state, and it toggles that same state whent it's checked/unchecked.
image.png

So far so good, but there is a problem..

This function has to be run on the first render to determine (checking localstorage and the media query) what the initial theme values should be. But the thing is, THAT first render doesn't occur on the user's machine, it occurs on the server side or the develepment machine.

So that's one of the key differences to take into account between traditional client side rendering and server side rendering.

By using just the previous code the users would get to see somthing like this every time they refresh the page:

darkmodeflash.gif

This is because the first render on the server delivers the exact same html for all the clients (with the default light mode), so if they have dark mode active they will get to see a flicker of the light mode.

How to solve this

Fortunately Gatsby has some server rendering APIs that allow us to make changes in different scenarions and during different stages of the server side rendering process. In this case we'll be using the gatsby-ssr.js API file.

The file gatsby-ssr.js lets you alter the content of static HTML files as they are being Server-Side Rendered (SSR) by Gatsby and Node.js.

For this approach we will use css variables and inject some javascript in the HTML before the rest of the content. This script will handle the initial theme mode, just as we were doing before, but this time it'll execute on the client side.

image.png

This function does the same than the previous one, but now it iterates through every color in the colors array, and with some nice dynamism, destructuring the objects in the array, it sets the css properties according to the selected mode.

Now we have to inject this function in the HTML, in the same file (gatsby-ssr) we turn the function into a string so we can inject it.

image.png

We are also making use of the onRenderBody API function to insert the script. This function allows us to insert components to the head and body of the html.js while the HTML is being built. It can take a lot of parameters to determine where and how to insert the code. In this case am passing just setPreBodyComponents, which I think is pretty much self descriptive.

You can check all the Gatsby ssr APIs in the official documentation.

What about the React context now?

image.png

Just a couple of tweaks and we have something similar to the ssr function, but here we get to manage the state. This function (setThemeMode) together with the themeMode state, are passed as a value in the Context Provider so we can use them later in the toggle component.

image.png

As you can notice I wrap the function inside a useMemo hook, this is so the function doesnt executes every time the component re-renders, unless one of its dependencies have changed (if the themeMode state is changed in this case).

Last but not least.. The styles

Now that the css variables are being injected and available for our use, changing the theme values is just as matter of modifying the theme.js file.

image.png

The styled components with css variables

No much to say here 😅 Now we can pass the css variables directly to our styled components, without worrying about handling context values in each and every component of our app.

Something cool about CSS variables is that they are reactive, so every time their values change the HTML is updated.

image.png

In this case for the global styles I use createGlobalStyle from styled components, but this approach works perfectly fine for regular css/scss too.

image.png

So that's it!

We have a nice looking dark mode that meets all the requirements. The users never get to see the wrong state, which is something pretty common I have seen in websites implementing dark mode.

There may be other simpler solutions, like gatsby-plugin-use-dark-mode and Theme-UI, but it all comes to the usecase you need. I like this approach because it doesn't compromise user experience while keeping the project lightweight and performant.

It also makes really easy to change the styles in components, and when adding new ones you dont need to worry about contexts or manipulating specific components, adding classes and so on. Just use the CSS variables you need and you are all set.

This is a work hard once / forget about it later approach 😄 so I think is really worth it.

Go check it out!

Demo: https://darkmode-styled.netlify.app/
Code: https://github.com/angelgarzz/dark-mode-styled-components
darkmodegatsby.gif

Sort:  

Thank you for this post. I am going to have to do this eventually for one of my react apps, and you've given me an idea on how to do it. It's not quite the same as your idea, but I think it might work.

Thanks for posting in the programming community!

I am glad you found it helpful in some way. Yeah, getting rid of the ssr part if it's pure React, but the css variables approach makes it really easy to mantain and make changes. Although I'm not sure I'll always go with this approach in a regular react app, instead of just passing the theme by context down the component tree.

Thanks to you for actually reading the thing! Is nice to get upvotes and tips(and appreaciated btw) but having someone to actually read and find some value in what you share is really cool.

Shared on Twitter #posh

image.png

Congratulations @angelgarz! You have completed the following achievement on the Hive blockchain and have been rewarded with new badge(s) :

You distributed more than 7000 upvotes. Your next target is to reach 8000 upvotes.

You can view your badges on your board and compare yourself to others in the Ranking
If you no longer want to receive notifications, reply to this comment with the word STOP

Do not miss the last post from @hivebuzz:

HiveBuzz supports meetups of the Hive UK Community
Feedback from the September 1st Hive Power Up Day