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 with
@media (prefers-color-scheme: dark)
to detect whether the user's device has enabled dark mode. - In the evening (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. The initial implementation worked well, but after a while, I discovered two serious issues:
- The toggle feature for light and dark modes implemented this way 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 be refreshed for both files to see the complete effect. Every few months, I also needed 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 toggle feature for light and dark modes these past few days.
Ver 0.1#
When I first rewrote the 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, allowing a switch to light or dark mode. Then, it simply loads the light.css, dark.css, and style.css files accordingly.
<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>
section to execute it 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 checks with window.matchMedia('(prefers-color-scheme: dark)').matches
to determine whether the device is in dark or light mode before switching to the opposite mode.
During local testing, the effect seemed fine, 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 added an extra CSS file compared to the initial approach, making it painful to add button styles later.
- I did not consider the priority issue when loading resources; CSS has the highest priority, while script has high priority. This caused the page's other CSS resources to load completely before executing the
<script>
code, which then began loading the crucial CSS file. With the loading time of CSS resources, there was a moment when the corresponding mode's CSS file had 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 later styles in 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 content of the :root
selector in the @media (prefers-color-scheme:dark)
when the device is in dark mode. When switching to dark or light mode, the class Dark
or Light
is added. At this point, the .Dark
/.Light
styles can override the earlier :root
styles.
Additionally, in the <head>
, I can directly reference the corresponding CSS file without needing to use JS for checks. Opening the performance tab in the developer tools shows that when executing the code inside <script>
, the Timings have not yet reached the First Paint (FP) timeline, so 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 loading issue due to incomplete loading. This problem can be solved using a preloading method by adding an onmouseover
event to the button, which adds <link rel="prefetch" href="">
to achieve resource preloading when the mouse hovers over it. Finally, remember to set XXX.onmouseover = null
to avoid repeated triggers.
Thus, the main framework of 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 addressed.
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 by Mix Space to xLog. The original link is https://www.vinking.top/posts/codes/dark-mode-css-implementation