Implementing 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 thedark
variant to any of the Tailwind classes and it will automatically apply the dark mode styling when thedark
class is added to thebody
tag. For example, the following will apply a dark background color to thebody
tag when thedark
class is added to it.
<body class="bg-white dark:bg-gray-800"></body>
- Better still, we can add a
dark
orlight
class to a parent object such as thehtml
orbody
and then define the behaviors of child tailwind css variables when either of these are set.
.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.
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
orlight
class to thehtml
orbody
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 inlocalStorage
. 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
.
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!