How to fix dark mode background color flicker in NextJS?

Viewed 2379

So my issue is that Next.js does not have access to localStorage on the client side and thus will ship HTML that by default either does or does not have class="dark".

This means that when the user reloads the page, <html> briefly does not have class="dark", causing a flash of light background color, before some javascript executes and class="dark" gets added to <html>. If I ship the HTML with class="dark", the same problem occurs but in reverse: then light mode users will experience a flash of dark background color before class="dark" gets removed from <html>.

Is there a way of executing some javascript before the page renders? Then I would be able to add or not add class="dark" to <html> based on the user's localStorage.

1 Answers

Sure, add a noflash.js file to your public directory with the following contents

(function () {
    // Change these if you use something different in your hook.
    var storageKey = 'darkMode';
    var classNameDark = 'dark-mode';
    var classNameLight = 'light-mode';

    function setClassOnDocumentBody(darkMode) {
        document.body.classList.add(darkMode ? classNameDark : classNameLight);
        document.body.classList.remove(darkMode ? classNameLight : classNameDark);
    }

    var preferDarkQuery = '(prefers-color-scheme: dark)';
    var mql = window.matchMedia(preferDarkQuery);
    var supportsColorSchemeQuery = mql.media === preferDarkQuery;
    var localStorageTheme = null;
    try {
        localStorageTheme = localStorage.getItem(storageKey);
    } catch (err) {}
    var localStorageExists = localStorageTheme !== null;
    if (localStorageExists) {
        localStorageTheme = JSON.parse(localStorageTheme);
    }

    // Determine the source of truth
    if (localStorageExists) {
        // source of truth from localStorage
        setClassOnDocumentBody(localStorageTheme);
    } else if (supportsColorSchemeQuery) {
        // source of truth from system
        setClassOnDocumentBody(mql.matches);
        localStorage.setItem(storageKey, mql.matches);
    } else {
        // source of truth from document.body
        var isDarkMode = document.body.classList.contains(classNameDark);
        localStorage.setItem(storageKey, JSON.stringify(isDarkMode));
    }
})();

// https://github.com/donavon/use-dark-mode/blob/develop/noflash.js.txt

Then, add the following script src tag to the returned contents wrapped within the Head class of your pages/_document file

import Document, {
    Head,
    Html,
    Main,
    NextScript,
    DocumentContext
} from 'next/document';

class MyDocument extends Document {
    static async getInitialProps(ctx: DocumentContext) {
        const initialProps = await Document.getInitialProps(ctx);
        return { ...initialProps };
    }
    render() {
        return (
            <Html lang='en-US'>
                <Head>
                    <meta charSet='utf-8' />
                    <script type="text/javascript" src='/noflash.js' />
                </Head>
                <body className='loading'>
                    <Main />
                    <NextScript />
                </body>
            </Html>
        );
    }
}

export default MyDocument;

This above approach works, but the following works perfectly with Nextv10+. It only requires the addition of the following config to your root next.config.js file.

next.config.js

module.exports = {
  env: {
    noflash: fs.readFileSync('/noflash.js').toString()
  }
}

Then, change the following script tag in your pages/_document file as indicated below

before

//
        <Head>
            <meta charSet='utf-8' />
            <script type="text/javascript" src='/noflash.js' />
        </Head>
//

after

//
        <Head>
            <meta charSet='utf-8' />
            <script type="text/javascript" dangerouslySetInnerHTML={{ __html: process.env.noflash}} />
        </Head>
//

Link to a repo where I use the first approach (from autumn 2020, before tailwindcss had built in dark mode support)

Related