需求
- 页面跳转时主题不要发生变化
- 如果是第一个页面,自动使用主题
- 默认 light 还是 dark
- 在一个浏览器选项卡中更改主题时,网站的所有其他选项卡也应随之更改
- 用户修改操作系统主题模式时,网站应该对此做出反应
- 根据时间变化自动切换主题
实现
在 <head>
中添加 <meta name="color-scheme" content="light dark">
浏览器只会改变那些没有主动设置颜色的元素
CSS 中通过 light-dark() 设置不同的颜色
CSS
:root {
color-scheme: light dark;
}
@media (prefers-color-scheme: light) {
.element {
color: black;
background-color: white;
}
}
@media (prefers-color-scheme: dark) {
.element {
color: white;
background-color: black;
}
}
- 使用 light-dark()
css
:root {
color-scheme: light dark;
}
.element {
/* fallback 的颜色,当用户浏览器不支持 color: light-dark(black, white); 时,回退到这个颜色 */
color: black;
/* light mode 下 color 用 black, dark mode 下 color 用 white */
color: light-dark(black, white);
background-color: white;
background-color: light-dark(white, black);
}
html
<html class="theme-light">
<form class="theme-selector">
<button
aria-label="Enable light theme"
aria-pressed="false"
role="switch"
type="button"
id="theme-light-button"
class="theme-button enabled"
onclick="enableTheme('light', true)"
>Light theme</button>
<button
aria-label="Enable dark theme"
aria-pressed="false"
role="switch"
type="button"
id="theme-dark-button"
class="theme-button"
onclick="enableTheme('dark', true)"
>Dark theme</button>
</form>
<!--- Rest of the website --->
</html>
scss
$theme-light-text-color: #111;
$theme-dark-text-color: #EEE;
@mixin color($property, $var, $fallback){
#{$property}: $fallback; // This is a fallback for browsers that don't support the next line.
#{$property}: var($var, $fallback);
}
p{
@include color(color, --text-color, $theme-light-text-color);
}
.theme-dark{
--text-color: #{$theme-dark-text-color};
}
JavaScript
// Find if user has set a preference and react to changes
(function initializeTheme(){
syncBetweenTabs()
listenToOSChanges()
enableTheme(
returnThemeBasedOnLocalStorage() ||
returnThemeBasedOnOS() ||
returnThemeBasedOnTime(),
false)
}())
// Listen to preference changes. The event only fires in inactive tabs, so theme changes aren't applied twice.
function syncBetweenTabs(){
window.addEventListener('storage', (e) => {
const root = document.documentElement
if (e.key === 'preference-theme'){
if (e.newValue === 'light') enableTheme('light', true, false)
else if (e.newValue === 'dark') enableTheme('dark', true, false) // The third argument makes sure the state isn't saved again.
}
})
}
// Add a listener in case OS-level preference changes.
function listenToOSChanges(){
let mediaQueryList = window.matchMedia('(prefers-color-scheme: dark)')
mediaQueryList.addListener( (m)=> {
const root = document.documentElement
if (m.matches !== true){
if (!root.classList.contains('theme-light')){
enableTheme('light', true)
}
}
else{
if(!root.classList.contains('theme-dark')) enableTheme('dark', true)
}
})
}
// If no preference was set, check what the OS pref is.
function returnThemeBasedOnOS() {
let mediaQueryList = window.matchMedia('(prefers-color-scheme: dark)')
if (mediaQueryList.matches) return 'dark'
else {
mediaQueryList = window.matchMedia('(prefers-color-scheme: light)')
if (mediaQueryList.matches) return 'light'
else return undefined
}
}
// For subsequent page loads
function returnThemeBasedOnLocalStorage() {
const pref = localStorage.getItem('preference-theme')
const lastChanged = localStorage.getItem('preference-theme-last-change')
let now = new Date()
now = now.getTime()
const minutesPassed = (now - lastChanged)/(1000*60)
if (
minutesPassed < 120 &&
pref === "light"
) return 'light'
else if (
minutesPassed < 120 &&
pref === "dark"
) return 'dark'
else return undefined
}
// Fallback for when OS preference isn't available
function returnThemeBasedOnTime(){
let date = new Date
const hour = date.getHours()
if (hour > 20 || hour < 5) return 'dark'
else return 'light'
}
// Switch to another theme
function enableTheme(newTheme = 'light', withTransition = false, save = true){
const root = document.documentElement
let otherTheme
newTheme === 'light' ? otherTheme = 'dark' : otherTheme = 'light'
let currentTheme
(root.classList.contains('theme-dark')) ? currentTheme = 'dark' : 'light'
if (withTransition === true && newTheme !== currentTheme) animateThemeTransition()
root.classList.add('theme-' + newTheme)
root.classList.remove('theme-' + otherTheme)
let button = document.getElementById('theme-' + otherTheme + '-button')
button.classList.add('enabled')
button.setAttribute('aria-pressed', false)
button = document.getElementById('theme-' + newTheme + '-button')
button.classList.remove('enabled')
button.setAttribute('aria-pressed', true)
if (save) saveToLocalStorage('preference-theme', newTheme)
}
// Save the state for subsequent page loads
function saveToLocalStorage(key, value){
let now = new Date()
now = now.getTime()
localStorage.setItem(key, value)
localStorage.setItem(key+"-last-change", now)
}
// Add class to smoothly transition between themes
function animateThemeTransition(){
const root = document.documentElement
root.classList.remove('theme-change-active')
void root.offsetWidth // Trigger reflow to cancel the animation
root.classList.add('theme-change-active')
}
(function removeAnimationClass(){
const root = document.documentElement
root.addEventListener(supportedAnimationEvent(), ()=>root.classList.remove('theme-change-active'), false)
}())
function supportedAnimationEvent(){
const el = document.createElement("f")
const animations = {
"animation" : "animationend",
"OAnimation" : "oAnimationEnd",
"MozAnimation" : "animationend",
"WebkitAnimation": "webkitAnimationEnd"
}
for (t in animations){
if (el.style[t] !== undefined) return animations[t] // Return the name of the event fired by the browser to indicate a CSS animation has ended
}
}
参考
A guide to implementing dark modes on websites | Koos Looijesteijn