September 17, 2020

🦄️ Web 站点暗色模式探索

#博文 #Vue.js #CSS3 #HTML5

本文存在一些DEMO,适合在 PC 端阅览

最近发布了自己的新博客 https://xlbd.me , 博客站点设计了暗色模式风格,但是当时只是基于媒体查询 perfers-color-schema 实现的跟随系统偏好设置切换主题风格,本次带来了可用户自定义的浅色/暗色主题风格切换功能,同时兼容跟随系统偏好设置切换主题风格。

# 跟随系统偏好设置切换

macOS Mojave 10.14+ 开始提供了外观设置选项,支持设置 浅色 / 深色 外观

macOS Catalina 10.15+ 开始可以设置 浅色 / 深色 / 自动 外观

用户随时都可以设定自己的系统外观,或者让系统在一天中从白天到晚上自动调整外观。随之而来的,perfers-color-schema CSS 媒体查询特性用于检测用户是否有将系统的主题色设置为浅色或者暗色。其值为 light / dark ,看一个例子:

代码很简单,声明两个媒体查询,编写相应媒体查询下色值即可。

/* 表示用户设定了浅色主题时,页面背景色为白色 */
@media (prefers-color-scheme: light) {
  body { 
    background: white;
  
}

/* 表示用户设定了暗色主题时,页面背景色为黑色 */
@media (prefers-color-scheme: dark) {
  body { 
    background: black;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 自定义主题切换

CSS 媒体查询 perfers-color-schema 已经帮我们实现了系统自动切换主题风格,虽然我们比较提倡在夜晚浏览网页或者使用APP时,用户看到的是暗色风格的UI界面,这样比较护眼。但是如果设计一个可让用户可选的主题开关,交互上会更好,用户也可以自由掌控到底想以什么主题浏览页面。

# 基于 CSS Variables 的实现方案

一、定义 CSS 变量

将 CSS 变量编写在根伪类 :root 里,它的作用域是整个 HTML文档,任何地方都能访问定义好的变量。下面我们分别定义浅色 / 暗色两套 CSS 变量:

:root {
  --color-light: rgba(0, 0, 0, .75);
  --color-dark: rgba(255, 255, 255, .75);
  --background-light: #f0f2f4;
  --background-dark: #242424;
  --border-light: rgba(0, 0, 0, .33);
  --border-dark: rgba(255, 255, 255, .33);
}
1
2
3
4
5
6
7
8

二、添加开关逻辑

开关逻辑核心功能其实就是标示当前用户的选择,我们需要一个状态来记录用户行为,使用 localStorage 保存状态;在 body 标签上设置一个属性 data-user-color-schema ,动态改变它的值。将这个属性作为选择器来使用。

const APP_THEME = 'user-color-scheme'

// set init theme mode as dark
localStorage.setItem(APP_THEME, 'dark')

const toggleButton = document.querySelector('.toggle-btn')

/**
 * if user select the theme, then use the given theme
 */
const applyTheme = givenTheme => {
  let currentTheme = givenTheme || localStorage.getItem(APP_THEME)
  
  if(currentTheme) {
    document.body.setAttribute('data-user-color-scheme', currentTheme)
  }
}

toggleButton.addEventListener('click', e => {
  e.preventDefault()
  
  applyTheme(toggleTheme())
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

三、动态 CSS 变量实现模式切换

不同选择器可以覆盖 CSS 变量的值,利用这个特性,可以获得动态的 CSS 变量。

:root {
  --color-light: rgba(0, 0, 0, .75);
  --color-dark: rgba(255, 255, 255, .75);
  --background-light: #f0f2f4;
  --background-dark: #242424;
  --border-light: rgba(0, 0, 0, .33);
  --border-dark: rgba(255, 255, 255, .33);
}

[data-user-color-scheme='light'] {
  --color-mode: 'light';
  --text-color: var(--color-light);
  --background-color: var(--background-light);
  --border-color: var(--border-light);
}

[data-user-color-scheme='dark'] {
  --color-mode: 'dark';
  --text-color: var(--color-dark);
  --background-color: var(--background-dark);
  --border-color: var(--border-dark);
}

body {
  padding: 2rem 1rem;
  color: var(--text-color);
  background: var(--background-color);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

到这里,我们就实现了用户自定义的主题切换功能,看下面例子中,点击切换按钮,body 中的颜色属性已经在动态改变了。

如果设置了自定义主题 user-color-schema 切换,那么 perfers-color-schema 的优先级就要降低了。因为用户的选择权要高于系统的偏好设置。

# 初始化主题模式

上面提到了 user-color-schema 优先级要高于 perfers-color-schema ,那么是不是说明 perfers-color-schema 媒体查询就没什么用了,并且也享受不到跟随系统偏好展示页面的功能了呢,其实我们有一个解决的方法,利用 window.matchMedia() API 来鉴别当前用户的系统外观偏好设置。

// 如果匹配到 perfers-color-schema: dark, 代表当前系统外观偏好设置为暗色
if (window.matchMedia) {
  const colorSchema  = window.matchMedia('(prefers-color-scheme: dark)')
	console.log(colorSchema.matches) // Boolean: true/false
}
1
2
3
4
5

通过这个方法我们就可以实现判断用户的系统是否设置了暗色外观,利用这一点我们可以帮助用户默认选择页面渲染模式。

# "鱼"和"熊掌"两者兼顾

虽然实现了跟随系统初始化主题模式,但是因为 use-color-schema 仍然优先级比 perfers-color-schema ,这时当你切换系统外观偏好的时候,页面是没有跟随改变的。

这时我们需要知道系统偏好何时发生变化,也就是我们期望知道 perfers-color-schema 的值何时发生变化,同样是基于 window.matchMedia() API,看下面例子:

const initTheme = () => {
  if (window.matchMedia) {
    const colorSchema  = window.matchMedia('(prefers-color-scheme: dark)')
    
		// 为媒体查询添加监听器
    colorSchema.addListener(e => {
      console.log(e.matches) // Boolean: true/false
      const currentTheme = e.matches ? 'dark' : 'light'
      localStorage.setItem(APP_THEME, currentTheme)
      toggleButtonMode.innerHTML = e.matches ? 'light' : 'dark'
      applyTheme(currentTheme)
    })
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 默认暗色模式

要实现站点默认是暗色模式,其实很简单,颜色反着写就OK了

body {
  background-color: black;
  color: white;
}

@media (prefers-color-scheme: light) {
  body {
    background-color: white;
    color: black;
  }
}
1
2
3
4
5
6
7
8
9
10
11

# Vue 3 中使用暗色模式

Vue3 的 composition API (组合式 API)提供了更好的逻辑复用和代码组织。

以下例子,我们可以将暗色模式变为响应式,一旦系统偏好改变了外观改变,我们可以立即获取改变的值。

# 抽离逻辑

定义 usePerfered

将公共逻辑媒体查询 window.matchMedia.matches 变为响应式进行抽象

// /src/helper/usePerfered.js
import { ref } from 'vue'
import { tryOnMounted, tryOnUnmounted } from './utils'

export function usePerferred (query) {
  let mediaQuery = null

  if (typeof window !== 'undefined') {
    mediaQuery = window.matchMedia(query)
  }

  const matches = ref(mediaQuery ? mediaQuery.matches : false)

  function handler(event) {
    matches.value = event.matches
  }

  tryOnMounted(() => {
    if (!mediaQuery) {
      mediaQuery = window.matchMedia(query)
    }
    handler(mediaQuery)
    mediaQuery.addListener(handler)
  })

  tryOnUnmounted(() => {
    mediaQuery.removeListener(handler)
  })

  return matches
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

组件中使用

我们得到了响应式的数据 isDarkmode ,它的变化会引起页面上的视图更新,可见 composition api 对业务逻辑的抽象是很有帮助的。

// /components/Darkmode.vue
import { reactive, watch, watchEffect, onMounted, toRefs } from 'vue'
import { usePerferred } from '../helper/usePerferred'

export default {
  setup () {
    const state = reactive({
      isDarkmode: usePerferred('(prefers-color-scheme: dark)')
    })
    ...
    return {
      ...toRefs(state)
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 颜色控制

# HSLa 介绍

HSL 色相-饱和度-亮度(Hue-saturation-lightness)模式,HSL 相比 RGB 的优点是更加直观:你可以估算你想要的颜色,然后微调。它也更易于创建相称的颜色集合。

比如下图描述了:hsl(200, 100%, 50%) 不同透明度的颜色表现

# 使用 CSS Variables 定制 HSLa

首先,我们将可控制的(Hue-saturation-lightness)拆分为独立变量,这样做的好处就是我可以单独改变某一个值去控制颜色的变化

:root {
  --text-color-h: 200;
  --text-color-s: 100%;
  --text-color-l: 50%;
}
1
2
3
4
5

将拆分的变量组合为 HSL 模式

:root {
  --text-color-h: 200;
  --text-color-s: 100%;
  --text-color-l: 50%;
  --text-color-hsl: var(--text-color-h), var(--text-color-s), var(--text-color-l);
}
1
2
3
4
5
6

基于定义好的 HSL,我们可以扩展颜色的不同透明度的色阶

:root {
  --text-color-h: 200;
  --text-color-s: 100%;
  --text-color-l: 50%;
  --text-color-hsl: var(--text-color-h), var(--text-color-s), var(--text-color-l);
  --text-color: hsla(var(--text-color-hsl), 1);
  --text-color-5: hsla(var(--text-color-hsl), .05);
  --text-color-10: hsla(var(--text-color-hsl), .1);
  --text-color-20: hsla(var(--text-color-hsl), .2);
  --text-color-30: hsla(var(--text-color-hsl), .3);
  --text-color-40: hsla(var(--text-color-hsl), .4);
  --text-color-50: hsla(var(--text-color-hsl), .5);
  --text-color-60: hsla(var(--text-color-hsl), .6);
  --text-color-70: hsla(var(--text-color-hsl), .7);
  --text-color-80: hsla(var(--text-color-hsl), .8);
  --text-color-90: hsla(var(--text-color-hsl), .9);
  --text-color-light: hsl(var(--text-color-h), var(--text-color-s), calc(var(--text-color-l) / .8));
  --text-color-dark: hsl(var(--text-color-h), var(--text-color-s), calc(var(--text-color-l) * .8));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 使用 HSLa 设定暗色主题

# 参考