4

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.

kyle_aoki
  • 247
  • 5
  • 6
  • 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? – Nico Apr 14 '21 at 17:26
  • 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: https://github.com/vercel/next.js/discussions/17631. – Eric Burel Apr 14 '21 at 17:43
  • 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: https://github.com/vercel/next.js/discussions/10724#discussioncomment-720. This is a client-first approach, you delay the rendering a bit with this. – Eric Burel Apr 14 '21 at 17:48

2 Answers2

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)

Andrew Ross
  • 1,094
  • 7
  • 16
  • Hi Andrew, google-verse led me here... with the latest NextJS v12 it triggers a build error: `Do not add – user2723025 Feb 25 '22 at 23:18
  • 1
    I've added a separate question on this as it seems rather hard in Next.js v12 without next-themes https://stackoverflow.com/questions/71277655/prevent-page-flash-in-next-js-12-with-tailwind-css-class-based-dark-mode – user2723025 Feb 26 '22 at 15:00
  • 1
    I think I solved it. Check out my answer to your question: https://stackoverflow.com/a/71470060/13199543 – Ronald Blüthl Mar 14 '22 at 15:16
  • Where does `fs` come from? Typescript throws an error when I try to implement this solution – jimmyNames Apr 26 '22 at 21:55
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.

  • 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 Review](/review/late-answers/32986388) – Daraan Oct 24 '22 at 20:24