Back when Theme UI was first introduced, the prefers-color-scheme
media query didn’t exist. You’d have a button on the website to switch between light and dark mode, or any other color scheme provided by the site’s developers.
This worked well, until the standardization of the color scheme media query. Nowadays, most websites implement color schemes by responding to the user’s preference via CSS.
To work with this new expectation, Theme UI introduced the system
color mode, which would respond to the prefers-color-scheme
media query.
However, this functionality is implemented in JavaScript, not in CSS, so there can be a color mode flash on initial page load, because the document is rendered before the JS bundle is evaluated.
Reading through the source code linked above, you can’t help but wonder why this system wasn’t scrapped entirely. It feels incredibly overengineered by today’s standards, where a CSS media query along with an event listener would do the trick.
It’d be great if we could opt out of this behavior, but alas, we can’t. So here’s a hack that will prevent the color flash to occur:
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.documentElement.classList.add('theme-ui-dark');
} else {
document.documentElement.classList.add('theme-ui-__default');
}
window
.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', (event) => {
if (event.matches) {
//dark mode
document.documentElement.classList.remove('theme-ui-__default');
document.documentElement.classList.add('theme-ui-dark');
} else {
//light mode
document.documentElement.classList.remove('theme-ui-dark');
document.documentElement.classList.add('theme-ui-__default');
}
});
Note that for this hack to work, it needs to block rendering, so you have to add it in a <script>
before the closing </head>
tag.
Methods will vary depending on the framework; I added it as a child of <Helmet>
for my site. As far as I can tell, there is no perceptible performance impact.
You might wonder how this hack even works. The reason is Theme UI ships a set of CSS custom properties for each color mode with the initial document request.
The CSS looks like this:
html {
--theme-ui-colors-text: #1c1917;
--theme-ui-colors-background: #fafafa;
}
html.theme-ui-__default,
.theme-ui-__default html {
--theme-ui-colors-text: #1c1917;
--theme-ui-colors-background: #fafafa;
}
html.theme-ui-dark,
.theme-ui-dark html {
--theme-ui-colors-text: #f5f5f5;
--theme-ui-colors-background: #171717;
}
If you open the devtools and switch between color schemes, you’ll notice that Theme UI replaces the values of the CSS custom properties set on the html
tag, the first block in the above example.
Our script overrides Theme UI’s handling of color modes entirely, by instead switching the class on the html
tag between theme-ui-dark
and theme-ui-__default
.
The higher specificity of classes mean that whatever is set on html
is irrelevant.
Be aware that Theme UI’s behavior could change in the future, make sure you read the release notes before upgrading major versions.