Everyone loves a well executed dark mode, me included. I think that having a full blast of white light in the middle of the night should be considered as bad as a slap in the face โ you're most definitely awake after getting it.
Hence, for the sake of my side project's future users and their night owl eyesight, I've recently implemented user controlled dark mode.
Fundamentally, if you want to leave it up to the system and stop the user from custmoizing their theme, we can just use the (prefers-color-scheme: dark)
match media and call it a day.
However, what kind of user experience would we be giving if we don't give them the option of blasting themselves with a light theme for self-inflicted torture? On the development side, it is most definitely easier to use toggling to check visual changes, so that is actually the main win here for implementing a theme switcher.
Now, onto the meat of the dark mode wizardry!
Set Tailwind Configโ
We're going to want to do some customizations on Ant Design components using the dark:
variant, hence we would want to enable dark mode in TailwindCSS. By default, it is already enabled, but it is set to the media
strategy, meaning that it will only respect the system preference and nothing else.
Of course, this is too restrictive for our needs, so lets change the strategy to something more flexible:
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: "class",
...
};
Here, we set the darkMode
key to "class"
, denoting that we want to utilize the dark
class in the html to determine if we want to display the dark theme or the light theme.
Determine the System Preferenceโ
Next, we need to determine the system's preference, which lets us then set the class on mount (because by default, your html will not have the dark
class).
export const isSystemDarkMode = ()=>{
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
}
And then somewhere within our React app, we use a useEffect
to manually set the class on our <body>
tag.
useEffect(() => {
if (
document &&
!document.body.classList.contains("dark") &&
isSystemDarkMode()
) {
console.log("setting dark mode")
document.body.classList.toggle("dark");
}
}, []);
Here, within the useEffect
, we trigger the callback on mount only (denoted by the 2nd argument []
. Thereafter, we check if the document's <body>
tag has a dark
class, and if not, we set the class manually.
Configuring Ant Designโ
Now, our prep work for determining Tailwind's theme is complete. However, Ant Design components are not getting themed correctly! This is because Ant Design components are by default using the default theming algorithm (light mode), and not the dark theme algorithm.
// determine if it is dark mode
const darkMode = document.body.classList.contains("dark")
and then in your render:
import { ConfigProvider, theme as antdTheme } from "antd";
// in the render
<ConfigProvider
theme={{
...theme,
algorithm: darkMode === true
? [antdTheme.darkAlgorithm]
: [antdTheme.defaultAlgorithm]
}}
>
This is the Ant Design <ConfigProvider>
, and we want to set the algorithm
key based on whether it is dark mode or not. In this case, the darkMode
variable is a computed value based on the presence of the dark
class. We then set the algorithm based on the computed value.
Implementing a Switcherโ
This switcher is going to update the document's <body>
class on each change.
// your event handler
const toggleDarkMode = (checked: boolean) => {
if (checked) {
document.body.classList.toggle("dark");
} else {
document.body.classList.remove("dark");
}
};
// in the render
<Switch
defaultChecked={darkMode}
onChange={toggleDarkMode}
/>
Conclusionโ
You can also store the user's preference within local storage or in app state. This is beyond the scope of this post, but do consider that you should always fallback to the system preference. Furthermore, due to TailwindCSS's class strategy, we cannot avoid setting the class on the <body>
tag (or any other tag in the html hierarchy). It is a necessary evil.
Caveatsโ
I have noticed that I am unable to apply default layer styling using the @layer
directive. Hence, styling would need to be placed anywhere lower than the node with dark
.