A long time ago, I wrote an article introducing the dark mode of a website.
At that time, I wanted to create a simple toggle feature for light and dark modes on the website. The idea was:
- During the day (from 6 AM to 6 PM), use pure CSS
@media (prefers-color-scheme: dark)
to detect whether the user's device has enabled dark mode. - At night (from 6 PM to 6 AM the next day), load another set of CSS files for nighttime use.
It seemed intuitive, simple, and convenient, and it solved the long-standing problem of being blinded by bright web pages at night. Initially, the effect was quite good, but after a while, I discovered two serious issues:
- The toggle feature for light and dark modes implemented with this method required maintaining two CSS files long-term, one for daytime and one for nighttime. This meant that every time a new feature needed to be added or an outdated style removed, both CSS files had to be updated simultaneously. The CDN cache also needed to refresh both files to see the complete effect. Every few months, I also had to compare the two files to check for any missing styles.
- The dark mode at night could not switch back to the light mode during the day. If styles were modified at night, it required manually changing the
<link>
content in the console to reload the daytime CSS file.
So, I started upgrading the light and dark mode toggle feature these days.
Ver 0.1#
When I first rewrote the light and dark mode toggle feature, I focused on the issue of "the dark mode at night cannot switch back to the light mode during the day," so I thought of using localStorage
to store the current page's light and dark mode status. Light
, Dark
, and System
correspond to light mode, dark mode, and system-following mode, respectively. The first load uses the system-following mode, and it can switch from the system-following mode to light and dark modes. Then, it simply loads the light.css, dark.css, and style.css files.
<link rel="stylesheet" id="linkCSS"/>
<script>
if (localStorage.getItem("Lighting")) {
if (localStorage.getItem("Lighting") === 'Light') document.getElementById('linkCSS').href = 'https://cdn.vinking.top/css/light.css';
else if(localStorage.getItem("Lighting") === 'System') document.getElementById('linkCSS').href = 'https://cdn.vinking.top/css/style.css';
else if(localStorage.getItem("Lighting") === 'Dark') document.getElementById('linkCSS').href = 'https://cdn.vinking.top/css/dark.css';
}else{
localStorage.setItem("Lighting", "System");
document.getElementById('linkCSS').href = 'https://cdn.vinking.top/css/style.css';
}
</script>
Place the above code in the <head>
so that it executes before the page renders, loading the corresponding CSS file. The toggle button works similarly, switching modes by changing localStorage
. If clicked while in system-following mode, it first uses window.matchMedia('(prefers-color-scheme: dark)').matches
to detect whether the device is in dark mode or light mode before switching to the opposite mode.
During local testing, it felt okay, but when deployed on the server, issues arose:
- The first issue was still the need to maintain multiple CSS files long-term. This method even required one more CSS file than the initial method, making it painful to add button styles later.
- I did not consider the priority issue when loading resources; CSS has the highest priority, and script has high priority. This caused the page's other CSS resources to load completely before executing the code inside
<script>
, which then began loading the critical CSS file. With the loading time of CSS resources, there would be a moment when the corresponding mode's CSS file was not loaded, significantly affecting the user experience.
After some modifications, the next version emerged.
Ver 0.6#
It is important to know that the differences between light.css, dark.css, and style.css lie in the different values of the :root
selector variables. Since this is the case, I can place the :root
content of the three files in the same file in a specific order. Different modes only need to add the corresponding class to <html>
to load. Based on the principle that styles later in the CSS will override earlier styles, I can write this order.
:root{
...
}
@media (prefers-color-scheme:dark){:root{
...
}}
.Light{
...
}
.Dark{
...
}
When first loading in system-following mode, <html>
does not add any class, allowing the use of the :root
selector and the @media (prefers-color-scheme:dark)
content when the device is in dark mode. When switching to dark mode or light mode, the class Dark
or Light
is added. At this point, the later .Dark
/.Light
styles can override the earlier :root
styles.
At the same time, in the <head>
, I can directly reference the corresponding CSS file without needing to use JS for judgment. Additionally, when opening the performance tab in the developer tools, I can see that when executing the code inside <script>
, the Timings have not yet reached the First Paint (FP) timeline. Therefore, when the page first paints, it will directly use the styles of the corresponding mode without any style missing issues.
<link rel="stylesheet" href='https://cdn.vinking.top/css/style.css'/>
<script>
if (localStorage.getItem("Lighting")) {
if (localStorage.getItem("Lighting") === 'Light') document.getElementsByTagName('html')[0].classList.add("Light");
else if(localStorage.getItem("Lighting") === 'Dark') document.getElementsByTagName('html')[0].classList.add("Dark");
}else{
localStorage.setItem("Lighting", "System");
}
</script>
Ver 1.0#
In actual use, there was an issue where clicking the toggle mode button resulted in a temporary background absence due to incomplete loading. This issue can be resolved using a preloading method by giving the button an onmouseover
event, adding <link rel="prefetch" href="">
to achieve resource preloading when the mouse hovers over it. Finally, remember to add XXX.onmouseover = null
to avoid repeated triggers.
Thus, the main framework for the light and dark mode toggle feature has been completed, and only minor issues like loading new code highlighting during the switch remain to be fixed.
Ps. A little complaint, why are beautiful horizontal images on Pixiv becoming harder to find? All the recommendations are vertical images...
This article is synchronized and updated to xLog by Mix Space. The original link is https://www.vinking.top/posts/codes/dark-mode-css-implementation