question on preventing css flickering when implementing dark mode #12533
-
I have a simple setup that persists a theme(such as dark mode) by using css variables and localStorage. My goal is to load the page with the correct theme, without the flashing of the default css theme(in this case it would be a light theme). Here's an example below that shows flashing as I refresh. The flashing is more prevalent upon loading routes. The way I prevented flashing was running a script before the content loads, to read local storage, get theme, and set css variables to the appropriate theme.
import Document, { Html, Head, Main, NextScript } from "next/document";
class MyDocument extends Document {
render() {
return (
<Html>
<Head>
<script src="/static/theme.js"></script>
</Head>
<body>
{/* some sources placed script inside body, before Main, but I experienced flashing */}
<Main />
<NextScript />
</body>
</Html>
);
}
}
export default MyDocument;
(function () {
var currentTheme;
function changeTheme(inputTheme) {
if (inputTheme === "dark") {
const theme = themeConfig.dark;
for (let key in theme) {
setCSSVar(key, theme[key]);
}
localStorage.setItem("theme", inputTheme);
} else {
const theme = themeConfig.light;
for (let key in theme) {
setCSSVar(key, theme[key]);
}
localStorage.setItem("theme", inputTheme);
}
}
function setCSSVar(property, color) {
document.documentElement.style.setProperty(property, color);
}
try {
currentTheme = localStorage.getItem("theme") || "light";
var themeConfig = {
dark: {
"--color-homepage-bg": "#0e141b",
"--color-text": "#fff",
"--code": "#ccc",
"--color-blue": "#f300e0",
"--color-grey": "#ccc",
},
light: {
"--color-homepage-bg": "#fff",
"--color-text": "#000",
"--code": "#f1f1f1",
"--color-blue": "#0070f3",
"--color-grey": "#eaeaea",
},
};
changeTheme(currentTheme);
} catch (err) {
console.log(new Error("accessing theme has been denied"));
}
})(); Here's the result with no flashing, as I refresh. Live Demo Even though I solved my problem, is it optimal or a bad solution? Here's the source code. |
Beta Was this translation helpful? Give feedback.
Replies: 7 comments 7 replies
-
Yeah this is a good solution, Dan did the same with his blog: https://github.com/gaearon/overreacted.io/blob/master/src/html.js#L21 But I would inline the JS like he did instead of making a request for it. |
Beta Was this translation helpful? Give feedback.
-
Yep, this looks good - basically the same thing that https://joshwcomeau.com/gatsby/dark-mode/ found as well, but as @rafaelalmeidatk mentions, inlining the JS is probably more correct. |
Beta Was this translation helpful? Give feedback.
-
I’ve used a similar trick in https://njt.now.sh, here is the commit: kachkaev/njt@1ef3bb0 Inlining JS is quicker than making a separate network request. To further reduce the network payload a bit, it is possible to compress inline JS with terser: // in _document.tsx:
import Terser from "terser";
import mem from "mem";
const minify = mem(Terser.minify);
const InlineJs: React.FunctionComponent<{ code: string; children?: never }> = ({
code,
}) => {
const minifyOutput = minify(code);
if (minifyOutput.error) {
throw minifyOutput.error;
}
if (!minifyOutput.code) {
throw new Error("Minified code is empty");
}
return <script dangerouslySetInnerHTML={{ __html: minifyOutput.code }} />;
}
// ...
<InlineJs code={`...`} /> Also, thanks to a hook from the use-dark-mode package, I got auto-syncing with the macOS theme without page reload 👀 |
Beta Was this translation helpful? Give feedback.
-
Excellent solution. |
Beta Was this translation helpful? Give feedback.
-
Wondering if this still works for everyone in Next.js 13. <body>
{/* The script here needs to run before the rest of the body to prevent default theme flicker */}
<MagicScriptTag />
<ThemeProvider>
<StoreProvider> const MagicScriptTag = () => {
console.log("in magic script tag...");
const boundFn = String(setColorsByTheme).replace("✸", JSON.stringify(COLORS));
let calledFunction = `(${boundFn})()`;
// eslint-disable-next-line react/no-danger
return <script dangerouslySetInnerHTML={{ __html: calledFunction }} />;
}; |
Beta Was this translation helpful? Give feedback.
-
I use a similar way in nextjs 13, It works but I need to use attribute instead of css variables,
const memoizedMinify = mem(minify);
export default async function RootLayout({ children }: PropsWithChildren) {
const setColorModeCode = await memoizedMinify(`
(function() {
if (window.localStorage.getItem('rhino.color_mode') === 'dark' || window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.documentElement.setAttribute('data-color-mode', 'dark');
}
}());
`);
return (
<html lang="en">
<head>
<script dangerouslySetInnerHTML={{__html: setColorModeCode.code!}}></script>
</head>
<body className={materialSymbolsOutlinedFont.variable}>
{children}
</body>
</html>
)
} |
Beta Was this translation helpful? Give feedback.
-
One other way to accomplish the same and prevent flickering with a pure JS implementation would be to inline the following
|
Beta Was this translation helpful? Give feedback.
Yeah this is a good solution, Dan did the same with his blog: https://github.com/gaearon/overreacted.io/blob/master/src/html.js#L21
But I would inline the JS like he did instead of making a request for it.