网页深色模式完整实现:从响应式设计到系统主题联动

需求

  • 页面跳转时主题不要发生变化
  • 如果是第一个页面,自动使用主题
  • 默认 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;
    }
}
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

相关推荐
西洼工作室1 天前
SSE与轮询技术实时对比演示
前端·javascript·css
Dontla1 天前
Tailwind CSS介绍(现代CSS框架,与传统CSS框架Bootstrap对比)Tailwind介绍
前端·css·bootstrap
向上的车轮1 天前
CSS 预处理器:Sass的基本用法、核心特性
css·sass
清灵xmf2 天前
CSS field-sizing 让表单「活」起来
前端·css·field-sizing
面向星辰3 天前
css选择器(继承补充)
前端·css
敲代码的嘎仔3 天前
JavaWeb零基础学习Day1——HTML&CSS
java·开发语言·前端·css·学习·html·学习方法
Tachyon.xue3 天前
Vue 3 项目集成 Element Plus + Tailwind CSS 详细教程
前端·css·vue.js
β添砖java4 天前
CSS网格布局
前端·css·html
EveryPossible5 天前
带有渐变光晕
前端·javascript·css
qianmo20215 天前
基于any2web+deepseek实现对三角函数定义的理解
css·html·css3