Dark Mode Rises

A beginner step-by-step guide on how to implement dark mode in Next.js app with Styled-compoments without `refresh flicker` using Rob Morieson’s technique with CSS variables.

Mike Gajdos
CodeX
7 min readSep 21, 2021

--

Photo by Serge Kutuzov on Unsplash

There are tons of blog posts about how to implement Dark Mode on a website. (Not just on Next.js one). While I was researching the subject before adding this feature to my site. I stumbled upon Rob Morieson’s article and loved his approach. I decided I will write about the implementation of his technique on my blog with my little tweaks.

I am not going to dwell on explaining what a dark mode is or why you want to add it to your site. Be it a design choice, energy savings consideration, or honoring the user's preference. Let's assume you want it.

While a dark mode toggling may seem simple on the surface, it can be done with just pure CSS. However, if you take into account accessibility and then further if the device or browser supports it, we should aim to display a user’s preferred theme on initial load and save their preference should they decide to switch themes.

Since the subtitle of this blog is about Next.js implementation, we first need to talk about how to work around constraints of server side rendering(SSR) and how the Next.js Apps are hydrated.

This is just a simple tutorial and by no means a comprehensive post (see the links at the end for some awesome in-depth dive into dark mode).

If you just would like to see a full code check my repo on GitHub here, otherwise …

Let’s jump into it

Initial Setup

I will use a boilerplate for the Next.js project created by create-next-app with StyledComponents.

Basic create-next-app provides a global CSS file which is the perfect location to define our CSS variables for dark and light mode. However, the bootstrapped example with styled-components does not provide us with a global CSS file, we can leverage styled-components createGlobalStyle function.

Even though we will be using StyledComponents for some styling, for our dark theme challenge we will venture into the land of CSS variables. The reasoning behind this approach will be explained later.

createGlobalStyle

createGlobalStyle is a helper function that will generate a special StyledComponent that handles global styles. Normally, styled-components are automatically scoped to a local CSS class and therefore isolated from other components. In the case of createGlobalStyle, this limitation is removed and things like CSS resets or base stylesheets can be applied.

createGlobalStyle returns a StyledComponent that does not accept children. Place it at the top of your React tree and the global styles will be injected when the component is "rendered".

Create a styles folder in your root directory.

Then import it into _app.js

Let’s add some styles for our darkMode theme.

As you might have guessed from the above CSS, we’ll be switching themes by applying a data-theme attribute to the <body> tag, rather than the traditional ThemeProvider approach.

I should point out that I have seen the approach applying it to <html> tag instead of the body. Credit to Rob Morieson( which this post is heavily inspired by) and Kent C. Dodds for explaining the difference in using CSS variables to ThemeProvider.

The gist of the CSS approach is the fact if we were to use ThemeProvider offered by StyledComponents which uses useContext hook under the hood, we would have to update the styles of every component, and then the browser will have to paint those updates.

But with the CSS Variables approach, we update the styles to a single component (the body), and then the browser paints just those updates. The browser paint should theoretically take the same amount of work on the part of the browser, so the only difference is how much work we’re making the browser do to have React re-render all our components and get emotion to update the styles of every component.

Let us tackle building our toggle component. We’ll create a new file called themeToggle.js and place it in the components directory.

I am using a couple of custom SVG Icons as our toggle theme indicators.

Icon Sun is commented out at the moment. Displaying it will be done later on by the click event.

After removing a bootstrapped content from the example template and importing our theme toggle button in index.js. This is how our index.js file looks like.

The basic static part of this task should render this :

Interactivity

With the initial and static stuff out of the way, it’s time to add the logic. We’ll start with React’s useState hook so we can store and update the active theme.

Our default theme is set to “light” — we’ll focus more on this later which will include consideration for a user’s prefers-color-scheme settings, along with some tweaks for persisting preferences on refresh using localStorage.

Let’s also add functionality with onClick event to the toggle button that updates our state accordingly.

Next, we’ll leverage React’s useEffect hook to set the data-theme attribute on the <body> tag. Adding [activeTheme] as a dependency in the dependency array means it will run anytime the active theme changes.

Finally, we need to use activeTheme to conditionally render our SVG Icons theme indicators

We should end up with something like this.

Accessibility

Accessibility is a topic of enormous dimension. For a coding newbie like myself, it is still something I need to learn a lot about.

There are awesome in-depth blog posts and tutorials on the matter. For simplicity of this blog….

  • Do not omit focus events and accessibility associated with it for screen readers.
  • Use aria-labels

Persisting Theme Preferences

If you select 'dark' mode then hit refresh, you'll notice that the website reverts to 'light' mode. This is an easy fix thanks to the localStorage property.

Above we’ve added a new useEffect hook that only runs on mount / unmount to check if a local storage item exists with the name 'theme'. If it does, then we set the active theme accordingly. We also update local storage any time the user toggles themes.

This is great, but if you switch to ‘dark’ mode and hit refresh, you might notice that we get a flash of the ‘light’ theme before the useEffect kicks in. This is the dreaded flicker we would like to eliminate. This happens due to Next's 'hydration' process, which you can read more about at nextjs.org.

Josh W. Comeau wrote an awesome blog post on dealing with this ‘flicker moment’ over on his blog. I have seen it called all sorts ( FART 👉Flash of inAccurate coloR Theme ). A Solution from Rob Morieson which moves from StyledComponents to CSS variable land will be somewhat more straightforward as we’ll be utilizing CSS variables (in contrast to his approach we stay within styled-components with the help of createGlobalStyle function )and data attributes on the <body> tag to provide theme values, but there are still a few steps involved, so no judgment if you want to call it job done at this point. Ok, maybe some judgment... that little flick on refresh doesn't look great.

Colour Scheme Preferences and a Dreaded Flash/Flick

We need to modify our custom _document.js file so we can inject a <script> tag into the <body>. This will allow us to set the theme before Next has a chance to 'hydrate' the markup.

Your local server requires restarting before Next will recognize the custom ‘document’. Check the Next.js docs to read more about customizing the ‘document’ page.

Let’s break down our next approach into few steps:

  • Checks to see if a user has already selected a theme by interacting with the toggle.
  • If not, then we’ll check if their browser /device has a preferred color scheme set.
  • Failing either of these checks we will default to our default ‘light’ mode.
  • Finally, we save the result of the above to the data-theme attribute on the <body> tag

To protect against XSS attacks, React DOM escapes any raw JavaScript before rendering. We are therefore required to embed our JavaScript using the dangerouslySetInnerHTML attribute. In this instance, it is safe as we have full control over the JavaScript being injected.

Now that we’re setting the data-theme attribute as the first order of call, we can go back to our ToggleTheme component and refactor how it retrieves its default value.

We no longer need the initial useEffect instead, we can now simply initialize our activeTheme state with the value of the data-theme attribute.

When you look at your page now you will get an `Error ` saying that document is not defined.

This is because Next is attempting to render the ToggleTheme component on the server, which has no reference to document - it's only available to the browser.

Thankfully Next.js has considered this and allows certain components to be dynamically imported at the browser level, without SSR.

Let’s update the way we import our ThemeToggle component into our index.js page to the 'dynamic' method, with ssr set to false.

That’s it! We now have a dark mode toggle that adheres to some accessibility best practices, persists on reload, and takes a user’s preferred color scheme into consideration. It also doesn’t suffer from the dreaded ‘flash’ of incorrect colors on the initial load.

I have to firstly and mostly give credit to Rob Morieson and his article on this matter. I just added few tweaks. Check it out here.

Just a few side notes :

Designing a color scheme is a challenge on its own. Right Contrast, right color palette… I am still learning 📚and I am still new to technical writing.🙇‍♂️

Resources

Originally published at https://www.justasemicolon.com.

--

--

Mike Gajdos
CodeX

A front-end developer💻 .Excels at finding new ways of confusing 🤦‍♂️ myself. #NeverStopLearning. I write for www.justasemicolon.com