How to fix dark mode background color flicker in NextJS?
Asked Answered
A

2

5

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.

Alannaalano answered 14/4, 2021 at 15:44 Comment(3)
Of course with next.js you can pre render the page (SSR), but are you willing to renounce to all CSR benefits only for that?Coming
You should not store this info in the localStorage but in a cookie, that's the only way to pass info alongside all requests and thus get Server side rendering to work as expected. This also means adding a getServerSideProps to parse the cookie and pass the color value as props, or getInitialProps in _app. I've written a proposal to make such use cases possible with static render only: github.com/vercel/next.js/discussions/17631.Venegas
Also to answer more directly the question, yes it's possible, you can run a pure JS script before the page renders. That's how Vercel dashboards avoid flickering on private pages when you are not logged in, that's exactly similar to your problem: github.com/vercel/next.js/discussions/…. This is a client-first approach, you delay the rendering a bit with this.Venegas
B
5

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)

Bog answered 14/4, 2021 at 21:43 Comment(4)
Hi Andrew, google-verse led me here... with the latest NextJS v12 it triggers a build error: Do not add <script> tags using next/head (see inline <script>). Use next/script instead. See more info here: https://nextjs.org/docs/messages/no-script-tags-in-head-component It seems to be immensely and unnecessarily difficult to add dark mode in v12 without the flash... any ideas? I'm lost :( When using next/script and strategy beforeInteractive it defers the script so still end up getting the flash...Swede
I've added a separate question on this as it seems rather hard in Next.js v12 without next-themes #71278155Swede
I think I solved it. Check out my answer to your question: https://mcmap.net/q/722172/-prevent-page-flash-in-next-js-12-with-tailwind-css-class-based-dark-modeSori
Where does fs come from? Typescript throws an error when I try to implement this solutionDisprove
S
1

My workaround is by conditional rendering in the first render,

function MyApp() {
    const [theme, setTheme] = useState(null);

    useEffect(() => {
        let theme = localStorage.getItem('theme') || 'light';
        setTheme(theme);
    }, []);

    if (!theme) {
        return; // `theme` is null in the first render
    }

    return (
        <Component {...pageProps} />
    );
}

for more detail, please see my answer.

Sped answered 20/10, 2022 at 4:14 Comment(1)
While this link may answer the question, it is better to include the essential parts of the answer here and provide the link for reference. Link-only answers can become invalid if the linked page changes. - From ReviewSpermato

© 2022 - 2024 — McMap. All rights reserved.