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

需求

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

相关推荐
AAA阿giao2 小时前
从零构建一个现代登录页:深入解析 Tailwind CSS + Vite + Lucide React 的完整技术栈
前端·css·react.js
掘金安东尼13 小时前
用 CSS 打造完美的饼图
前端·css
掘金安东尼20 小时前
纯 CSS 实现弹性文字效果
前端·css
前端Hardy1 天前
HTML&CSS&JS:打造丝滑的3D彩纸飘落特效
前端·javascript·css
前端Hardy1 天前
HTML&CSS&JS:丝滑无卡顿的明暗主题切换
javascript·css·html
parade岁月2 天前
Tailwind CSS v4 — 当框架猜不透你的心思
前端·css
前端Hardy2 天前
HTML&CSS&JS:基于定位的实时天气卡片
javascript·css·html
程序员阿耶2 天前
5 个让 CSS 起飞的新特性,设计师看了直呼内行
css
前端Hardy2 天前
HTML&CSS:纯CSS实现随机转盘抽奖机——无JS,全靠现代CSS黑科技!
css·html
漂流瓶jz3 天前
BEM、OOCSS、SMACSS、ITCSS、AMCSS、SUITCSS:CSS命名规范简介
前端·css·代码规范