Web界面深色模式和主題化開發

DevUI是一支兼具設計視角和工程視角的團隊,服務於華爲雲 DevCloud平臺和華爲內部數箇中後臺系統,服務於設計師和前端工程師。
官方網站: devui.design
Ng組件庫: ng-devui(歡迎Star)
官方交流羣:添加 DevUI小助手(微信號:devui_official)進羣

引言

深色模式(Dark Mode)在iOS13 引入該特性後各大應用和網站都開始支持了深色模式。在這以前,深色模式更常見於程序IDE開發界面和視頻網站界面。前者經過下降屏幕亮度,使得使用人員長時間盯着屏幕眼睛沒有那麼疲憊;後者經過深色模式來降噪,從而突出主體內容部分。快速開發一個深色模式難嗎? 在支持css自定義屬性(又稱css變量,css variables)的現代瀏覽器裏,能夠說是至關的容易。甚至能夠在運行時實時新增主題,擺脫傳統css主題文件加載模式下的主題須要預編譯內置不能隨時修改的弊端。下面咱們來看一下如何使用css自定義屬性來完成深色模式和主題化的開發。javascript

主題切換器開發

首先咱們須要打通一套支持css自定義屬性的開發模式。css

CSS自定義屬性使用

這裏簡單介紹一下CSS自定義屬性,有時候也被稱做CSS變量或者級聯變量。它包含的值能夠在整個文檔中重複使用。自定義屬性使用 --變量名:變量值來定義,用var(--變量名[,默認值]) 函數來獲取值。舉一個簡單例子:html

<!--html-->
<div><p>text</p></div>

/\* css \*/
div { --my-color: red; border: 1px solid var(--my-color); }
p { color: var(--my-color); }

這時候div的邊框和內部的p元素就能使用這個定義的變量來設置本身的顏色。前端

一般CSS自定義屬性須要定義在元素內,經過在:root僞類上設置自定義屬性,能夠在整個文檔須要的地方使用。CSS變量是能夠繼承的,也就是說咱們能夠經過CSS繼承建立一些局部主題,這裏就不展開局部主題的討論,咱們只須要使用好:root僞類就能對整站實施主題化了。java

如何切換主題呢,咱們在運行的時候給頭部插入一段<style>:root{--變量1: 色值1;--變量2: 色值2 ;……}</style>,並經過id或者引用的方式保持對該style元素的引用,經過修改style元素innerText爲 :root{--變量1: 色值3; --變量2: 色值4;……}就能夠成功替換變量顏色了。git

因爲主題數據多是從接口等其餘地方獲取的,咱們能夠在使用的地方給它先加上默認值,避免主題數據到達以前出現沒有顏色的現象,好比 p { color: var(--變量1,色值1);}這樣,就使用上了css自定義屬性來在運行時動態加載不一樣的主題顏色值。github

Sass/Less支持

若是直接在開發css中使用css變量很容易因爲書寫問題,定義問題最後致使變量衆多,管理困難,變動默認色值替換成本高等問題。在大型網站的開發中一般會用sass/less來預約義一些顏色變量來進行色彩管理。算法

在使用sass和less的時候能夠改變原來的傳遞色值方式改成傳遞css自定義屬性和默認值。color定義文件:npm

image.png

這裏有個反作用就是,一旦色值被定義爲var變量,則這個var表達式就沒法再被less/sass的色彩計算函數所計算使用,這塊咱們在後面的章節再進行討論。segmentfault

定義完對應的變量以後, 使用的地方就能夠直接使用使用這些變量,方便統一管理。

使用媒體查詢

prefer-color-scheme是瀏覽器獲取系統上用戶對顏色主題的傾向性的css api,使用該api咱們就能夠輕鬆使得網站的主題跟隨系統的顏色設置展現不一樣的顏色了。

css的API以下:

// css
@media (prefers-color-scheme: light) {
  :root{--變量1: 色值1;--變量2: 色值2; ……}
}
@media (prefers-color-scheme: dark) {
  :root{--變量1: 色值3; --變量2: 色值4; ……}
}

腳本方面也有對應的媒體查詢方案,js的API以下:

// js
function isDarkSchemePreference(){
  return window.matchMedia('screen and (prefers-color-scheme: dark)').matches;
}

主題切換服務

最後咱們須要寫一個主題服務,主要目的就是支持在切換主題的時候應用不一樣的css變量數據,假定咱們的css變量的數據存儲在一個對象裏,key值爲css變量名,value值爲css變量在該主題下的值,那麼咱們的主題切換服務的關鍵核心函數以下:

// theme.ts
export class Theme {
  id: ThemeId;
  name: string;
  data: {
    \[cssVarName: string\]: string
  };
}

// theme-service.ts
class ThemeService {
  contentElement;
  eventBus;
  // ……
  applyTheme(theme: Theme) {
    this.currentTheme = theme;
    if (!this.contentElement) {
      const styleElement = document.getElementById('devuiThemeVariables');
      if ( styleElement) {
        this.contentElement = <HTMLStyleElement>styleElement;
      } else {
        this.contentElement = document.createElement('style');
        this.contentElement.id = 'devuiThemeVariables';
        document.head.appendChild(this.contentElement);
      }
    }
    this.contentElement.innerText = ':root { ' + this.formatCSSVariables(theme.data) + ' }';
    document.body.setAttribute('ui-theme', this.currentTheme.id);

    // 通知外部主題變動
    this.notify(theme, 'themeChanged');
  }

  formatCSSVariables(themeData: Theme\['data'\]) {
    return Object.keys(themeData).map(
      cssVar => ('--' + cssVar + ':' + themeData\[cssVar\])
    ).join(';');
  }

  private notify(theme: Theme, eventType: string) {
    if (!this.eventBus) { return; }
    this.eventBus.trigger(eventType, theme);
  }
}

其中applyTheme函數會建立一個style元素,若是已經建立好了則直接改變style的內容。若是要支持跟隨系統還須要一些額外函數的判斷,這裏就不展開了,能夠參考連接,原理是經過動畫結束事件監聽媒體查詢變化,對應可使用enquirejs庫。

至此咱們打通了主題服務和css變量值在開發中的應用,下面就能夠開發一個深色模式了。

深色模式開發

語義化色彩變量

深色模式涉及到了大量網站視覺的「反色」,在已有的網站當中,應該好好排查和梳理網站的顏色,把顏色歸一和約束到必定的變量範圍和數量裏,並給顏色的不一樣使用場景一個不一樣的語義變量名,這樣能取得場景分離的效果。

從文本顏色上咱們舉個簡單的例子:

一般的網站裏都會有正文(主要文本),幫助提示信息(次要文本),文本佔位符。這裏咱們可使用三個變量來描述這些文本text-color-primary,text-color-secondary,text-color-tertiary,也可使用text-color-normal,text-color-help-info,text-color-placeholder來描述這這些顏色值。

這裏強烈建議使用更有語義的變量而不是色值自己的描述,好比:錯誤背景色,應該使用background-color-danger而不是background-color-red,由於對於不一樣的主題顏色值多是不同的。

圖1 語義化變量示意

使用統一語義變量控制組件表現

須要定義多少的變量才恰當,這個取決於網站的色彩空間約束範圍和使用場景的定義粒度。當定義了一套變量以後咱們就能夠對組件/網站的不一樣組成部分進行變量統一。

好比搜索框和下拉框,使用一樣的變量控制相同部分的表現,使得組件在主題變化的可使用相同的顏色規則。

圖2 使用變量對組件進行規約

提供暗黑主題色值

完成了上面重要的兩步,咱們就能夠經過給變量提供一套新的色值來達到主題的變化了。

圖3 經過色值的切換實現深色主題切換

圖片的處理

圖片的處理並不能像文字同樣地去反轉顏色或者反轉亮度,這樣可能照成不適。一般若是有準備亮色和暗色兩套圖片,能夠採用變量化圖片地址在不一樣主題下切黑圖片。若是圖片來自用戶輸入,其餘地方的截圖,這時候須要稍微處理一些下降亮度。圖片簡化地獲取當前的主題狀態能夠在body上增長一個ui主題是不是深色模式的屬性。

深色方案一:圖片增長透明度。適用場景:簡單文章圖片和純色背景。

// css
body\[ui-theme-mode='dark'\] img {
  opacity: 0.8;
}

深色方案二:帶圖片的位置疊加一個灰色半透明的層,適用場景:背景圖,非純色背景等。

// css
body\[ui-theme-mode='dark'\] .dark-mode-image-overlay {
  position: relative;
}
body\[ui-theme-mode='dark'\] .dark-mode-image-overlay::before {
  content: '';
  display: block;
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(50, 50, 50, 0.5);
}

前者不適用與帶有背景圖片的層處理,也不適合經過疊加圖片遮擋來呈現效果的處理,可是用在文章博客中的插入圖片很是簡單有效,圖片能夠天然地疊加到純色深色的背景色上。後者給了另外一種方案完成背景層的疊加,但對代碼有必定的入侵。

提供主題變化訂閱應對第三方組件場景

經過以上幾個基本的步驟就能在編碼的過程當中經過使用變量指定顏色值,得到主題的能力。可是面對大量第三方組件,有本身的主題,也可能有本身的深色主題,這塊再去入侵式地修改爲自定義的變量工做量不小且並不必定合適。

這時候須要提供主題訂閱,在主題發生變化的時候,得到通知,而後給第三方組件設置必定對應的變動。

咱們須要一個簡單的eventbus,實現方式不限。這裏給出一個簡單版本的接口以下:

// theme/interface.ts
export interface IEventBus {
  on(eventName: string, callbacks: Function): void;
  off(eventName: string, callbacks: Function): void;
  trigger(eventName: string, data: any): void;
}

切換主題的時候發出themeChanged事件,使用on監聽就可以得到當前主題變動事件,經過判斷主題,給第三方的組件套上對應的主題,或者修改js顏色變量等等。

降級支持和使用腳本膩子

降級PostCSS插值腳本

一旦使用了var以後,那些不支持var的老瀏覽器會顯示爲無顏色,這裏咱們使用postcss插件處理最後一個階段的css。

// postcss-plugin-add-var-value.js
var postcss = require('postcss');
var cssVarReg = new RegExp('var\\\\\\\\(\\\\\\\\-\\\\\\\\-(?:.\*?),(.\*?)\\\\\\\\)', 'g');

module.exports = postcss.plugin('postcss-plugin-add-origin-css-var-value', () => {
  return (root) => {
    root.walkDecls(decl => {
      if (decl.type !== 'comment' && decl.value && decl.value.match(cssVarReg)) {
        decl.cloneBefore({value: decl.value.replace(cssVarReg, (match, item) => item) });
      }
    });
  }
});

該postcss插件經過遍歷css規則裏的帶有var(--變量名, 變量值)在該行的上一行插入了一行替換爲直接變量值的值,兼容不支持css var的瀏覽器。

image.png

css-vars-ponyfill 使 IE9+ 和 Edge 12+支持上主題切換

css-vars-ponyfill 這個npm包可使得ie9+/edge12+支持上css自定義屬性,它是一個帶有選項的兼容方案,大概原理就是經過監聽style裏帶有var自定義屬性的值,替換爲原值並插入。該兼容方案目前不兼容直接掛在在元素上的局部的css自定義屬性定義。該方案還提供了實時監聽style插入的選項,支持var鏈式的取值。簡單地加入polyfill就可使用了。

// polyfill.ts
import cssVars from 'css-vars-ponyfill';
cssVars({ watch: true, silent: true});

一些問題的探討

什麼網站須要開發深色模式?

深色模式適合長時間閱讀、長時間沉浸式瀏覽的網站,包括新聞、博客、知識庫等文章瀏覽和視頻網站,開發IDE界面等沉浸式交互。這些網站使用深色模式能夠經過下降亮度減小對眼睛的刺激,減小長時間瀏覽的疲憊和暈眩的感受。

深色模式不適合一些非深色風格產品的展現,深沉的背景色會影響產品風格呈現、傳遞的情感和用戶觀看時候的心情,不適當的顏色搭配容易引發反感。像一些電商網站深色模式要慎重處理,深色可能會使得產品圖片呈現的積極風格受到必定程度的抑制,顏色可能會影響用戶的購物慾望。一些主題推廣宣傳類的網站也是,顏色可能會削弱主題的表達。

有沒有更簡單的深色模式映射切換?好比使用HSL替代RGB色值。

HSL色值的表達形式是經過色相、飽和度、亮度,既然深色模式是調整亮度和飽和度,那是否能夠經過hsl色值來自動計算呢? 這種自動出暗色版本的色值還有待探索中,主要有兩個緣由:1)深色模式的溫馨度不是線性亮度和飽和度映射能完成的,顏色的函數計算深色映射顯得相對單調。2)實際狀況是一個顏色可能會映射到多個暗黑場景的顏色。

針對第一點,目前有一些UI會推出非線性反色的算法,也是爲了解決顏色一塊兒調整亮度以後變得看不清、色彩反色後衝擊過大的問題。這類的算法還有不少優化空間。在淺色搭配狀況下可能很好看的顏色,放到深色下可能就會引發不溫馨:不恰當的對比度會引發視覺上看不清晰;不恰當的色彩碰撞會引發反感;不恰當的飽和度、亮度會顯得UI有點髒。

針對第二點,能夠舉如下的場景來講明:一樣是白色,有色背景下的白色,在深色模式下可能仍是保持白色;而做爲背景色的白色在深色場景下會對應調整爲深色。

圖4 一種白色的存在切換主題的多種映射

此時,自動經過色值計算就須要區分顏色的周邊顏色或者底層疊加顏色來計算,這無疑加大了計算難度。

因此這塊自動計算並不太容易,還須要一些的探索。

Sass/Less使用var變量後變成字符串管理,沒法對顏色進行變換計算?

自己sass/less的變量和css自定義屬性就不是一套變量系統,sass/less的是一種編譯型變量(編譯時肯定值,編譯後不存在),而css是一個運行時變量(即運行時肯定值)。用sass/less去管理css變量時爲了管理css變量防止定義失誤,但使用了Sass或Less以後替換成var以後會發現,sass和less是一些好比lightenfadeoutrgba等等的函數都沒法使用了,由於對與sass和less來講,var(--xxx, #xxx)是一個字符串不是顏色值。這塊目前也沒有比較好的方法, 有一些文章也討論了一些解法,如 連接,大致的思路是拆分顏色的表達爲hsl形式,而後對顏色的維度進行操做處理,實際上仍是不能無感知地使用內建的色彩變換函數。另外一個解法/方案是:把涉及顏色變換的地方統一處理而後再賦予新的css變量名,再也不在mixin等函數裏對顏色進行變換而是對變量名進行規則變化。若是讀者有其餘較好的思路也能夠在評論裏分享。

總結

本文介紹了利用CSS自定義屬性可以給css定義一些顏色變量,輕鬆地實現深色主題的開發甚至支持更多的主題化。經過色彩變量定義,使用變量,處理圖片和處理三方組件支持實現整站的深色模式的規約和完善。進一步介紹了降級支持的方法,並對深色模式的適用範圍和一些其餘方式實現進行了討論。

加入咱們

咱們是DevUI團隊,歡迎來這裏和咱們一塊兒打造優雅高效的人機設計/研發體系。招聘郵箱:muyang2@huawei.com。

文/DevUI rhlin

往期文章推薦

《淺談前端中的圈複雜度》

《手把手教你搭建一個灰度發佈環境》

《手把手教你使用Vue/React/Angular三大框架開發Pagination分頁組件》

相關文章
相關標籤/搜索