There is a lot to think about when implementing a dark mode theme on a website. We have a huge guide on it. There are some very clever quick wins out there, but there are also some quite tricky things to pull off. One of those tricky things is how it’s not a dark mode “toggle” between dark and light, but really three modes you need to support: dark, light, and use system preference. That’s similar to how audio preferences work in many apps, which allow you to very specifically choose which audio input or output you want, or default to the system preference.
CSS and JavaScript can handle the system preference angle, via the prefers-color-scheme
API, but if the user preference has changed, and that preference is now different than the user preference, you’re in the territory of “Flash of inAccurate coloR Theme” or FART. Ok ok, it’s a tounge-in-cheek acronym, but it’s potentially quite a visually obnoxious problem so I’m keeping it. It’s in the same vein that FOUT (Flash of Unstyled Text) is for font loading.
Storing a user preference means something like a cookie, localStorage
, or some kind of database. If access to that data means running JavaScript, e.g. localStorage.getItem('color-mode-preference');
, then you’re in FART territory, because your JavaScript is very likely running after a page’s first render, lest you’re otherwise unnecessarily delaying page render.
You can access a cookie with a server-side language before page-render, meaning you could use it to output something like <html class="user-setting-dark-mode">
and style accordingly, which deftly avoids FART, but that means a site that even has access to a server-side language (Jamstack sites do not, for example).
Allllll that to say that I appreciated Rob Morieson’s article about dark mode because it didn’t punt on this important issue. It’s very specifically about doing this in Next.js, and uses localStorage
, but because Next.js is JavaScript-rendered, you can force it to check the user-saved preference as the very first thing it does. That means it will render correctly the the first time (no flash). You do have to turn off server-side rendering for this to work, which is a gnarly trade-off though.
I’m not convinced there is a good way to avoid FART without a server-side language or force-delayed page renders.
How bad is this that it is not even mentioned?
const theme = localStorage.getItem(‘color-mode-preference’) || ‘system’
document.body.classList.add(`theme-setting-${theme}-mode`)
That’s the whole point. That JavaScript is very likely to run after the page has rendered.
Ou. I forgot to mention to put this right after opening tag. Sorry about that.
const theme = localStorage.getItem(‘color-mode-preference’) || ‘system’
document.body.classList.add(`theme-setting-${theme}-mode`)
Now I see others suggest this too.
That acronym. Amazing.
A tricky way to work around this could be to use a service worker and the caching API to update the css file retrieved on request. It’d be something kinda interesting.
This might actually be a good use for service worker, especially if you’re already using it. If somebody has explicitly picked a preference for a dark or light mode, you can intercept their network request for the main style sheet (which would have the ‘prefers-color-scheme’ media queries) and over-write it with a cached version of the style sheet that only has the styles for the specified colour scheme.
I’m not sure I get the part about localstorage. Next.js is React on Server-Side (Node.js) so you should use Cookie to do this and not localStorage as it’s only available on the client-side.
Here is a basic implementation of this on Nuxt.js. The equivalent of Next.js but for Vue. Using a Cookie and doing this on the server-side is actually much better as it doesn’t prevent the page rendering until the JS is parsed and executed.
You could put something like this little script in the pages head and i don’t think the delay is to much.
Sites like https://mxb.dev/ or mine use that technique already.
How about hiding the page completely before the user theme preference is known? Use a mid grey as an interim and then fade to the light or dark theme when possible…
That’s what I mean by force delay. Feels like a hefty price to pay for this.
Also, ‘no-js-no-content’ is one of the most horrible developments on the web over the past years.
If you must hide the page, use something along the lines of
body.preloader
in the markup; and in the CSS:Yeah. I’d like to see this issue less neglected. I’d even settle for setting body background to dark by default, as – in case of FART – people preferring bright colours will not get hurt as much as if the reverse should happen.
Having a small inline JS in your html header that loads the user preference from a cookie or localStorage doesn’t seem like a huge delay in rendering IMO.
Yeah I would agree. I’ve also seen a small script tag placed as the first element inside the body tag where theme preferences are checked and acted upon. I wouldn’t say that it impacts rendering in a noticeable way…at least compared to everything else we do on a daily basis
Just make sure to put the dark mode CSS class before any
<link rel="stylesheet">
and<style>
tag:I’m not convinced flashes of inaccurate color themes are the big problem. From what I can see:
a flash of white that happens before a dark theme loads is fantastically unpleasant in a dark room with the lights off.
a flash of black screen viewed outside in the daytime merely looks like a section of the screen turned off for a fraction of a second.
Considering this asymmetry, wouldn’t it be better to re-think of this problem as “flashes of white are to be avoided at some cost; flashes of black are much, much less bad” and to start writing stylesheets that are dark-mode first?
I haven’t yet worked on sites where FOUT is something I’ve been tasked to fix, but how about something like this:
That way, pages get a white-on-black color scheme before the browser bothers to paint anything and nobody gets blinded in bed.
Hi, I’ve attempted to find a workaround for this problem.
https://medium.com/@lyxsus/fixing-fart-flash-of-inaccurate-color-theme-for-static-websites-7935f7f85e20
I did this for a while by putting a script tag at the beginning of the doc. It helped bypass the flash without much of a noticeable performance hit. Wrote about it here
https://blog.jim-nielsen.com/2018/icon-galleries-dark-mode/
Granted, it requires JavaScript. Eventually I removed this functionality by removing the UI toggle for dark mode and relying solely on prefers-color-scheme, which bypasses the need for server side rendering or JS trickery to persist the state of a dark or light mode toggle.
There is now a new proposal to prevent FART: https://github.com/WICG/user-preference-media-features-headers
This would allow adding user preferences (such as preferred color scheme) to the initial request header.