6 min read

Implementing Dark Mode in Gatsby + Tailwind while Avoiding Flash of Unstyled Content (FOUC)

How to implement dark mode in Gatsby + Tailwind while avoiding flash of unstyled content (FOUC)

Flash of Unstyled Content is that dreaded moment when your website loads and the content appears to flicker or jump around as css gets loaded. No bueno. This can especially happen (as in my case) when implementing light and dark mode. In this post, I will walk through the following :

  • What is Flash of Unstyled Content (FOUC)
  • Implementing Dark Mode in Gatsby+Tailwind
  • Avoiding Flash of Unstyled Content (FOUC) with gatsby-ssr.js (the most important part)

This post is in part based on the dark mode tutorial from Jeff Jadulco, who incidentally also wrote the original Gatsby theme used for this website.

What is Flash of Unstyled Content

Flash of Unstyled Content (FOUC) is a phenomenon that occurs when a web page is briefly rendered in the browser's default styles prior to loading an external CSS stylesheet, JavaScript file, or web font. . The effect of this is that the user sees a flash of content in the default style (which may be no styling at all and hence ugly ) before the page is fully loaded. This can be a jarring experience for users and is something that can make the website feel unprofessional.

Something that makes this error even more pesky is that in the Gatsby workflow you might not see this behavior during development (gatsby develop) but suddenly see it after you deploy your site (gatsby build && gatsby serve). This is because the development server is able to load the CSS files faster than the production build.

Implementing Dark Mode in Gatsby + Tailwind

Tailwind rocks and the documentation for integrating it into Gatsby is pretty straightforward. I will not go into the details of how to do this here but you can check out the official documentation for more details. At the end of this process you should have a global.css file which is integrated into your app via gatsby-browser.js and gatsby-ssr.js as shown below.

To implement dark mode with Tailwind offers a simple mental model.

  • Tailwind intrinsically supports dark mode via the dark variant. This means that you can add the dark variant to any of the Tailwind classes and it will automatically apply the dark mode styling when the dark class is added to the body tag. For example, the following will apply a dark background color to the body tag when the dark class is added to it.
html
<body class="bg-white dark:bg-gray-800"></body>
  • Better still, we can add a dark or light class to a parent object such as the html or body and then define the behaviors of child tailwind css variables when either of these are set.
css
.dark {
--color-bg-primary: #2d3748;
--color-bg-secondary: #283141;
--color-text-primary: #f7fafc;
--color-text-secondary: #e2e8f0;
--color-text-accent: #81e6d9;
}
.light {
--color-bg-primary: #ffffff;
--color-bg-secondary: #edf2f7;
--color-text-primary: #2d3748;
--color-text-secondary: #4a5568;
--color-text-accent: #2b6cb0;
}

In the css snippet above, we define the colors for tailwind bg-primary, bg-secondary, text-primary, text-secondary and text-accent variables for both dark and light modes. To use these variables, we'd need to extend the tailwind config file to include these variables as shown below.

js
module.exports = {
purge: [],
darkMode: "class", // or 'media' or 'class'
theme: {
extend: {
colors: {
"bg-primary": "var(--color-bg-primary)",
"bg-secondary": "var(--color-bg-secondary)",
"text-primary": "var(--color-text-primary)",
"text-secondary": "var(--color-text-secondary)",
"text-accent": "var(--color-text-accent)",
},
},
},
variants: {
extend: {},
},
plugins: [],
};

At this point, if you add the dark class to the body tag, the bg-primary color will be set to #2d3748 and the bg-secondary color will be set to #283141. Similarly, if you add the light class to the body tag, the bg-primary color will be set to #ffffff and the bg-secondary color will be set to #edf2f7.

Simply swithcing between the dark and light class will toggle between the two color schemes across the entire site. Sweet!

  • Finally, we can write code to toggle the addition of the dark or light class to the html or body tag. Ideally, we want to set, track and persist some variable that indicates the user's preference for dark or light mode. In our Gatsby React App, we perhaps want to use a Context Provider as this allows to access this variable from any component in our app. This implementation should ideally also persist the preference across page loads possibly by storing it in localStorage. An example implementation is here.

Avoiding Flash of Unstyled Content (FOUC) with gatsby-ssr.js

Once you have dark mode setup and can toggle between, this is where most tutorials end. However, the challenge is that post-build, there is the chance that your page data is loaded before the dark mode is applied. This means that the user will see a flash of unstyled content (FOUC) before the dark mode is applied.

To avoid this, our goal is to add the right dark or light class to to your page e.g. body or html tag before the page is rendered. This can be done by adding the following code to gatsby-ssr.js.

js
import { createElement } from "react";
const applyDarkModeClass = `
(function() {
try {
var mode = localStorage.getItem('darkmode');
document.getElementsByTagName("html")[0].className = mode === 'dark' ? 'dark' : 'light';
} catch (e) {}
})();
`;
export const onRenderBody = ({ setPreBodyComponents }) => {
const script = createElement("script", {
dangerouslySetInnerHTML: {
__html: applyDarkModeClass,
},
});
setPreBodyComponents([script]);
};

In the snippet above, we fetch the dark mode setting from local storage and then apply the appropriate class to the html tag. This means that the dark mode is applied before the page is rendered and hence avoids the flash of unstyled content (FOUC) when the page is built.

Importantly, this snippet needs to be in gatsby-ssr.js and not gatsby-browser.js because we want to apply the dark mode before the page is rendered. I was originally setting dark mode in my app layout component (on page load using useEffect) but this was too late and hence the FOUC.

Happy coding!

Interested in more articles like this? Subscribe to get a monthly roundup of new posts and other interesting ideas at the intersection of Applied AI and HCI.

RELATED POSTS | web, react

Read the Newsletter.

I write a monthly newsletter on Applied AI and HCI. Subscribe to get notified on new posts.

Feel free to reach out! Twitter, GitHub, LinkedIn

.