Implementing a Simple Dark Mode with CSS Filter
- frontend
The Manis theme used on this blog does not provide native support for dark mode, so I decided to add it myself. Initially, I thought about customizing the CSS with media queries, but it seemed like a bigger change. I did a quick search and found that someone had already proposed the idea of using CSS Filter to implement a simple dark mode, and there was even code that could be directly applied to a Hugo blog. Compared to media queries, using CSS Filter not only simplifies the implementation, but also allows users to switch between light and dark mode without adjusting the system/browser’s global settings.
In the specific CSS implementation, I first used invert(1)
to invert the colors of the entire webpage, but this also caused a reversal of color tones. Therefore, I used hue-rotate(180deg)
to bring the tones back. However, while this operation is suitable for text, it affects the display of images, videos, and other elements, making them appear as if they were being X-rayed. So, I had to apply invert(1) hue-rotate(180deg)
again to these elements that needed to be excluded from the dark mode, in order to revert them back to normal.
html {
background-color: #ebebeb !important;
}
html {
filter: invert(100%) hue-rotate(180deg);
}
/* using not to exclude certain elements */
img:not(.icon-text, .icon-social),
video,
code {
filter: invert(100%) hue-rotate(180deg) contrast(100%);
}
To allow users to switch between light and dark mode, an additional toggle icon needs to be introduced. When clicked, it will insert/remove the CSS tag for dark mode and save the user’s preference to localStorage
. If the user has not explicitly set a preference, the system/browser’s global dark mode setting should be followed. Therefore, I used window.matchMedia
to detect it.
var toggle = document.getElementById("dark-mode-toggle");
var darkTheme = document.getElementById("dark-mode-theme");
// probe system default dark mode setting
var systemDefault = null
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
systemDefault = "dark";
} else {
systemDefault = "light";
}
// use user preference if possible
var savedTheme = localStorage.getItem("dark-mode-storage") || systemDefault;
setTheme(savedTheme);
toggle.addEventListener("click", () => {
if (toggle.src.endsWith("/img/moon.svg") ) {
setTheme("dark");
} else if (toggle.src.endsWith("/img/sun.svg") ) {
setTheme("light");
}
});
function setTheme(mode) {
localStorage.setItem("dark-mode-storage", mode);
if (mode === "dark") {
darkTheme.disabled = false;
toggle.src = "/img/sun.svg";
} else if (mode === "light") {
darkTheme.disabled = true;
toggle.src = "/img/moon.svg";
}
}
The complete modifications can be seen in my Pull Request for implementing dark mode in this theme. A simple example can be found in this Gist. The final result is as follows:
Update on 12/27: The PR has been accepted and merged.