javascriptreactjsnext.jsthemesuser-experience

How to fix dark mode background color flicker in NextJS?


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.


Solution

  • 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)