<html data-darkmode='light'>
<!-- ... -->
<button id="dark-switch">
<i id="moon-icon"></i>
<i id="sun-icon"></i>
</button>
<!-- ... -->
</html>
/*
利用高优先级选择器 [data-darkmode='dark']
覆盖原有样式来实现两个 icon 的显示隐藏
* Use the high priority of selector [data-darkmode='dark']
to overwrite the original style to implement icon switching.
以下两组样式表选择器优先级分别是:
1. #sun-icon | 优先级 1.0.0 | display none
2. [data-darkmode='dark'] #moon-icon | 优先级 1.1.0 | display none
当根标签 data-darkmode='dark' 时覆盖 3 -> #moon-icon | 优先级 1.0.0 | display block
3. #moon-icon | 优先级 1.0.0 | display block
4. [data-darkmode='dark'] #sun-icon | 优先级 1.1.0 | display block
当根标签 data-darkmode='dark' 时覆盖 1 -> #sun-icon | 优先级 1.0.0 | display none
*/
#sun-icon,
[data-darkmode='dark'] #moon-icon {
display: none;
}
#moon-icon,
[data-darkmode='dark'] #sun-icon {
display: block;
}
const defaultTheme = 'light' // 'light' | 'dark'
// 获取浏览器 localStorage 存储的 theme 值
const currentTheme = localStorage.getItem('theme')
const getPreferTheme = () => {
// 如果浏览器 localStorage 存储存在 theme 值就返回本地值
if (currentTheme) return currentTheme
// 否则返回默认值
if (defaultTheme) return defaultTheme
// 或者返回用户设备的模式值
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
}
let themeValue = getPreferTheme()
const setPreferTheme = () => {
localStorage.setItem('theme', themeValue)
reflectPreference()
}
const reflectPreference = () => {
// 设置根标签 data-darkmode 的值
document.documentElement.dataset.darkmode = themeValue
const body = document.body
if (body) {
const computedStyles = window.getComputedStyle(body)
const bgColor = computedStyles.backgroundColor
// 设置 body 的背景颜色到使用 theme-color 的 meta 标签
document.querySelector('meta[name="theme-color"]').setAttribute('content', bgColor)
}
}
// 阻塞页面渲染,提前设置好 data-darkmode 值避免首屏加载闪烁
reflectPreference()
// 同步自系统的夜间模式
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', ({ matches: isDark }) => {
themeValue = isDark ? 'dark' : 'light'
setPreferTheme()
})
// 页面已加载完成……
const darkSwitch = document.getElementById('dark-switch')
darkSwitch.addEventListener('click', () => {
// 节流,避免快速切换造成错乱
darkSwitch.disabled = true
if (darkSwitch.disabled) return
// 设置 data-darkmode 值并存入 localStorage 的 theme 字段
dataset.darkmode = dataset.darkmode === 'light' ? 'dark' : 'light'
globalThis.localStorage.setItem('theme', dataset.darkmode)
// 200ms 的禁止切换
setTimeout(() => {
darkSwitch.disabled = false
}, 200)
})