Categories
程式開發

如何給網站添加暗黑模式


暗黑模式是系統級別的

所謂的暗黑模式並不是現在才有的,這個事實已經存在很久了。如果你很早就接觸過電腦的話,你可能會發現你使用過的電腦屏幕經歷過好幾個過程,看起來會像下面這樣:

如何給網站添加暗黑模式 1

是不是覺得既熟悉又陌生。既然如此,為什麼今年會成為設計或者說Web端的一個熱點呢?其實這一切都應該歸功於Apple公司,在macOS系統中提出了dark和light兩種視覺模式,即 暗色(dark)高亮(light) 兩種皮膚,而且這兩種皮膚是系統級別的,我們可以通過系統上的切換,讓整個電腦上只要支持dark/light模式的應用都可以輕易切換。

如何給網站添加暗黑模式 2

那麼為什麼要從系統級別去做這個事情呢?這是有原因的。系統面對的用戶群體中朋部分人士在身體上存有一定的缺陷,比如說色盲的用戶群體。也就是說,這種暗黑模式或者高亮模式對於有色盲的用戶群體是非常友好的。既然如此,為了讓自己的Web網站或者Web應用能向系統級別靠齊,就有了網站級別的暗黑模式。你可能在很多網站的右上角看到了一個提供暗黑和高亮模式的切換按鈕。

暗黑模式實現原理

給Web網站或者Web應用添加暗黑模式的基本原理我想大家應該很清楚,事實上也非常的簡單。

如何給網站添加暗黑模式 3

正如上圖所示,給同一個Web網站或Web應用提供多套皮膚,用戶根據自己的喜歡進行選擇。那麼給網站添加暗黑模式是同一個原理,就是給網站同時提供兩套皮膚,即theme1.css和theme2.css。

如何給網站添加暗黑模式 4

早期我們可能會藉助於JavaScript腳本,根據用戶的選擇在一個標籤上進行兩個主題文件(即.css文件)切換來實現:




// Script
document.getElementById('buttonID').addEventListener('click', function(){                    document.getElementById('theme_css').href = '../theme2.css'; 
})

這可能是一種比較古老的實現方案。也是大家最為熟悉的方案。

CSS實現暗黑模式切換

時至今日,給Web網站或Web應用程序實現暗黑模式已有多種模式。可以是純CSS的方式,也可以是CSS和JavaScript結合的模式。那麼接下來,我們來看看具體的實現方式。

媒體查詢prefers-color-scheme

CSS有一個特別強大的特性,那就是媒體查詢@media,CSS的@media規則可以用於有條件地將樣式應用於文檔以及其他各種上下文和語言,如HTML和JavaScript。在W3C的Media Queries Level 5【1】引入了 “用戶首選媒體特性”,即 Web網站或應用程序檢測用戶顯示內容的首先方式的方法。 (https://drafts.c​​sswg.org/mediaqueries-5/

比如prefers-reduced-motion這個媒體查詢就可以檢測頁面上的動畫,假設設備開啟了“Reduce motion”選項,就可以通過該媒體查詢選項讓頁面上的元素是否具有動效:

如何給網站添加暗黑模式 5

如果用戶開啟減少動效的喜好,那麼就不要在元素上使用動效:

@media (prefers-reduced-motion: reduce) {
    button {
        animation: none;
    }
}

如果用戶沒有在系統級別設置該選項的話,可以像下面這樣讓按鈕有動效:

@media (prefers-reduced-motion: no-preference) {
    button {
        animation: vibrate 0.3s linear infinite both;
    }
}

Web上的其他具有動效的元素都可以像上面之樣使用, 上面只是用button為例。

如果Web網站有很多元素具有動效的話,還可以將所有與動效相關的CSS放在一個獨立的文件中,然後通過link的media屬性來加載:


為了說明JavaScript如何控制preferences-reduced-motion。這裡假設你在項目中使用了 Web Animation API【2】。當用戶開啟了偏好設置,CSS規則會被瀏覽器動態觸發,這樣一來我五一需要自己監聽變化,然後手動停止與動畫相關的東西:(https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API

const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
mediaQuery.addEventListener('change', () => {
    console.log(mediaQuery.media, mediaQuery.matches);
    // ...
});

如果你有強迫症,強迫減少網站上所有有動效停下來,還可以像下面這樣的簡單粗暴的操作:

@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        animation-duration: 0.001s !important;
        transition-duration: 0.001s !important;
    }
}

有關於prefers-reduced-motion更詳細的介紹可以閱讀@Thomas Steiner的博文《 Move Ya! Or maybe, don’t, if the user prefers-reduced-motion!》【3】。 (https://developers.google.com/web/updates/2019/03/prefers-reduced-motion

似乎上面的內容偏離了我們今天要聊的主題,大家不用著急。只不過是拿prefers-reduced-motion這個媒體查詢來拋磚引玉而以。在CSS中通過媒體查詢的prefers-color-scheme特性和prefers-reduced-motion類似,不同是,該特性是用於檢測用戶是否要求頁面使用light還是dark主題。該媒體查詢常見的值有:

  • no-preference:表示用戶未指定操作系統主題。其作為 布爾值 時以false輸出
  • light:表示用戶的操作系統是淺色主題(light)
  • dark:表示用戶的操作系統是深色主題(dark)

也就是說,通過prefers-color-scheme媒體查詢要讓暗黑模式(dark)開啟深色系主題,可以像下面這樣使用:

@media (prefers-color-scheme: dark) {
    :root {
        --background-color: #111416
        --text-color: #ccc;
        --link-color: #f96;
    }
}

當然在非dark模式下,你的樣式可能像下面這樣:

:root {
    --background-color: #fff;
    --text-color: #333;
    --link-color: #b52;
}

body {
    background-color: var(--background-color);
    color: var(--text-color);
}

a {
    color: var(--link-color);
}

注意上面提供的示例代碼僅僅是最基本的顏色配置方案,但也可以說完成了近90%的工作。但細節決定成敗。如果要讓你的Web網站或應用程序在light和dark模式切換下能有較好的效果,還需要注意其他的一些細節,比如說img、svg等元素的細節處理。有關於細節方面的,稍後我們會再討論,暫且不表。

正如上面的示例所示,我們是通過CSS的媒體查詢特性來檢測dark模式,即通過檢查媒體查詢是否首選。那麼顏色方案是否匹配還需要檢查當前瀏覽器是否支持dark模式。

我們可以像下面這樣來檢測瀏覽器是否支持dark模式:

if (window.matchMedia('(prefers-color-scheme)').media !== 'not all') {
    console.log('浏览器支持dark模式!(^_^)');
}

至於哪些瀏覽已支持prefers-color-scheme特性,我們可以通過Caniuse來查詢【4】:(https://caniuse.com/prefers-color-scheme/embed

如何給網站添加暗黑模式 6

前面的示例簡單的向大家演示瞭如何給Web網站或應用程序設置暗黑模式。但有很多細節我們需要去注意。

在一個應用中只dark(暗色系)和light(亮色系)只能是二選一,永遠不可能兩者共存。為什麼要提這個呢?我們從加載策略來做衡量。如果我們不管三七二十一,直接將所有樣式(普通樣式、亮色系樣式和暗色系樣式)都用一個.css文件加載的話會強迫用戶在關鍵的渲染路徑中下載CSS(包括你不想要的模式代碼也加載進來了)。為此,為了優化加載速度和給用戶提供更好的體驗,我們可以將CSS分成三個部分,以延遲非關鍵的CSS:

  • style.css:網站上普通樣式(通用樣式)
  • dark.css:暗色系所需樣式規則
  • light.css:亮色系所需樣式規則

其中dark.css和light.css可以通過有條件的加載。加上並不是所有瀏覽器都已支持prefers-color-scheme特性,所以我們在加載通用樣式style.css規則的基礎上動態默認加載light.css。即,不支持該特性的瀏覽器會按下面的順序加載CSS:style.css ➜ light.css ➜ dark.css;如果支持該特性的瀏覽器則會按下面的順序加載CSS:style.css ➜ dark .css ➜ light.css。具體的代碼如下:

-->

按照該規則,前面的CSS示例代碼,我們就可以按下面這樣的文件來劃分:

// dark.css:root {    --background-color: #111416    --text-color: #ccc;    --link-color: #f96;}// light.css:root {    --background-color: #fff;    --text-color: #333;    --link-color: #b52;}// style.cssbody {    background-color: var(--background-color);    color: var(--text-color);}a {    color: var(--link-color);}

這裡使用了CSS自定義屬性,該示例再次向大家演示了CSS自定義屬性的強大之處。

CSS的新特性color-scheme

CSS Color Adjustment Module Level 1【5】提供了另一個新屬性color-scheme。該特性會告訴瀏覽器該應用的顏色主題和允許用戶代理的特殊變體樣式表,而且它還可以讓Web中的部分區域的渲染在dark和light之間切換,比如讓瀏覽器渲染渲染的表單域是個黑色背景和高亮文本。 (https://caniuse.com/#feat=mdn-html_elements_meta_name_color-scheme

如何給網站添加暗黑模式 7

俗話說,百聞不如一見,這裡向大家展示一個由@Thomas Steiner提供的案例【7】:(https://twitter.com/tomayac

上面這個案例和以往提供的案例有所不同。該案例按前面所講的分成三個獨立的樣式文件:style.css、dark.css和light.css。嘗試切換暗黑模式並重新加載頁面,你會發現不匹配的樣式文件仍然會被加載,只是優先級有所差異,這樣做它們就不會與站點當前所需的資源競爭。

當網站是在light模式下,樣式文件加載優先級是style.css ➜ light.css ➜ dark.css,即 dark.css 權重最低(Lowest):

如何給網站添加暗黑模式 8

當網站在dark模式下,樣式文件加載優先級是style.css ➜ dark.css ➜ light.css,即 light.css權重最低(Lowest):

如何給網站添加暗黑模式 9

當瀏覽器不支持prefers-color-scheme而且設置light為默認模式,那麼樣式加載優先級會和高亮模式一樣:

如何給網站添加暗黑模式 10

特別聲明,上面三圖截圖來自於《Hello darkness, my old friend》【8】一文。 (https://web.dev/prefers-color-scheme/

上面示例還做了另一個細節上的優化。和其他媒體查詢更改一樣,可以通過JavaScript的訂閱來更改暗黑模式。比如可以動態更改頁面的favicon或更改來決定Chrome中URL欄的顏色。代碼並不復雜:

const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');darkModeMediaQuery.addListener((e) => {    const darkModeOn = e.matches;    console.log(`Dark mode is ${darkModeOn ? 'on' : 'off'}.`);});

CSS混合模式來助攻

上面我們看到的都是原生CSS處理暗黑模式的技術方案。事實上我們還可以通過CSS Hack來實現,採用CSS的filter和CSS的混合模式mix-blend-mode。

下面這個示例就是CSSfilter實現的暗黑模式:

Switch from light to dark mode using the toggle [CSS filter]@airenCodePen【9】(https://codepen.io/airen/pen/qzBzXB

關鍵代碼很少:

.theme-dark {     filter: invert(100) hue-rotate(180deg); } .theme-dark img {     filter: invert(100) hue-rotate(180deg); }

另外還可以使用CSS的mix-blend-mode來實現:

.dark-mode-screen {
    width: 100vw; 
    height: 100vh; 
    position: fixed; 
    top: 0; 
    left: 0; 
    background: white; 
    mix-blend-mode: difference; 
}

如果你想讓頁面中部分元素忽略mix-blend-mode:difference帶來的影響,可以使用isolation: isolate:

.twitter-logo,
.emoji {
    isolation: isolate; 
}

效果會類似下圖這樣:

如何給網站添加暗黑模式 11

有關於這方面的詳細介紹可以閱讀@thoughtspile的《How to create a dark theme without breaking things: learning with the Yandex Mail team》【10】和的@wgao19《Night Mode with Mix Blend Mode: Difference》【11】 。 (https://habr.com/en/company/yandex/blog/450032/)(https://dev.wgao19.cc/sun-moon-blending-mode/

其他細節

根據前面的內容去操作,不管是使用CSS的媒體查詢prefers-color-scheme、新特性color-scheme還是藉助CSS的濾鏡filter或混合模式mix-blend-mode都可以輕易的給Web網站或應用程序添加暗黑模式。

注意,很多同學有一個小誤區,認為filter和mix-blend-mode只能用於圖片,事實上並非如此,他可以運用於Web的各種元素上。

那麼掌握了實現暗黑模式的技術方案就能做出好的效果嗎?並非如此,其中還是有很多細節需要我們注意。比如顏色的配置、Web媒體(圖片、Icon等)和可訪問性等方面的處理都值得我們去推敲。

顏色的配置

在給Web網站或Web應用設計暗黑模式的時候,你千萬不要鑽到死胡同里。暗黒模式並不僅僅是 黑(black)白(white) 之間的切換。你想像一下,在一個深夜密不透光的地方,用你的肉眼注視著一塊高亮的屏幕,時間久了,你會有什麼樣的一個感覺:

如何給網站添加暗黑模式 12

正確的做法是應該為你的品牌色系提供一個暗色係版本,如果不奏效的話,可以根據需要在黑色和灰色之間選擇一個平均顏色。比如說,Web的背景顏色是black(#000)(或者接近#000)的話,建議你前景色(比如文本顏色)取值為rgb(250,250,250)(或者靠近這個顏色值)。這樣才能讓你的整體效果不至於亮瞎用戶的眼睛。比如下面這樣的一個效果:

如何給網站添加暗黑模式 13

如果你實在拿不准配色是否合理(Web安全顏色),你可以藉助在線工具,比如 Contrast Checker 【12】:(https://contrastchecker.com/

如何給網站添加暗黑模式 14

該工具是根據 WCAG 2.0 guidelines for contrast accessibility【13】 標準來做的。比如下面圖所展示的效果就是一個較好的效果:(https://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast-contrast.html

如何給網站添加暗黑模式 15

除了借助工具來檢測之外,瀏覽器的插件也是把利器,可以藉助瀏覽器有關於Accessibility相關的插件來做檢測,就比如小站,檢測出來的結果令人汗顏:

如何給網站添加暗黑模式 16

事實上,很多網站都存在這樣的缺陷:

如何給網站添加暗黑模式 17

圖片處理

在暗黑模式下,圖片的處理也是非常重要的。它們可能會直接影響用戶的體驗,太亮的圖像可能會讓用戶感到困惑和不舒服。而且有人做過這方面相應的調查,大多數被調查的人在暗黑模式下更喜歡亮度低的圖像。比如下面這張圖:

如何給網站添加暗黑模式 18

左側是暗黑模式下效果,右側是在高亮模式下效果。

為了能更用戶更好的體驗,這裡提倡在不同模式下給用戶展示不同效果的圖像,但並非說在不同的模式下引入不同的圖片。就目前CSS的技術,我們在同一圖像源下可以很好的對圖像做處理。比如,粗暴一點的使用CSS的opacity,溫柔一點的使用CSS的混合模式mix-blend-mode(如果是背景的話則用background-blend-mode)或 filter。

// 粗暴模式
@media (prefers-color-scheme: dark) {
    img {
        opacity: 0.65;
    }

    img:hover {
        opacity: 1;
    }
}

// 温柔模式
@media (prefers-color-scheme: dark) {
    img {
        filter: brightness(.8) contrast(1.2);
    }
}

不過這裡有一個細節需要注意,我們引入的圖像源有可能是.svg的矢量圖,如果希望給矢量圖(更多是Icon)一個不同於位圖(更多是圖像,照片)的重新著色處理。那麼我們可以通過屬性選擇器和偽類選擇器將.svg過濾掉:

@media (prefers-color-scheme: dark) {
    img:not([src*=".svg"]) {
        filter: brightness(.8) contrast(1.2);
    }
}

如果你希望給用戶更多的選擇的話,我們可以將CSS自定義屬性和JavaScript結合起來,可以讓用戶根據自己的喜好去做調節。這裡還是拿圖片的處理為例吧:

// dark.css
:root {
    --brightness: brightness(.8);
    --contrast: contrast(1.2);
    --image-filter: var(--brightness) var(--contrast);
}

// JS
document.documentElement.style.setProperty('--image-filter', value);

也可以參考下面這個Demo【14】,使用filter修改圖片效果:(https://codepen.io/nitnelav/full/jgwRNJ

CSS [email protected]【15】(https://codepen.io/nitnelav

當然,如果你是位追求極致的同學,希望在暗黑模下給用戶提供最好的圖像,而不是隨便修改圖片的亮度或飽和度;但又不希望因加載圖片資源過多而影響整體的性能(甚至不希望因為自己的原因過渡浪費流量)。如果真的是這樣的話,你可以由設計師為暗黑模式下提供特定的圖片,然後與元素一起使用。可以在的根據媒體屬性的設置加載所需要的圖片資源:


    
    
    map

暗黑模式下會加載dark.webp圖片,在高亮模式或者不支持prefers-color-scheme的瀏覽器中會加載light.webp圖片,不支持的瀏覽器會加載light.png圖片。你將看到的效果可能如下:

如何給網站添加暗黑模式 19

圖標的處理

剛才提到過,很有可能你Web網站或Web應用程序中有很多Icon圖標用的是SVG圖標。在暗黑模式下,同樣要對Icon圖標做相應的處理。這裡來看兩種情景。

先來看第一種,那就是.svg文件和其他格式的圖像一相通過標籤引入。由於該Icon很有可能是純色的,因此在暗黑模式下,我們可以通過filter來做dark/light之間的切換:

/* dark.css */
:root {
    --icon-filter: invert(100%);
    --icon-filter_hover: invert(40%);
}

img[src*=".svg"] {
    filter: var(--icon-filter);
}

/* light.css */
:root {
    --icon-filter_hover: invert(60%);
}

/* style.css */
img[src*=".svg"]:hover {
    filter: var(--icon-filter_hover);
}

如果你還想調整成其他的顏色,還可以像下面這個 Demo 【16】來操作,增加filter的屬性值選項:(https://codepen.io/airen/full/eaRBXq

Change Icon Color with CSS Filter (Forked @ Cassie Evans)@airenCodePen【17】(https://codepen.io/airen/pen/eaRBXq

該方法和處理圖像的方法是類似的。接下來我們再來看第二種方式。使用的Icon圖標很有可能是內聯的SVG,針對這樣的場景,我們可以使用CSS的currentColor屬性。 currentColor最大的特性就是可以根據color的值來決定元素的顏色,而對於SVG繪製的Icon圖標,主要由path、circle、rect這樣的元素構成,這些元素可以通過fill、stroke來決定填充色和描邊色。換句話說,我們在使用內聯SVG時,將SVG中用到fill和stroke的屬性值都強制設置成currentColor,就像下面這樣:

 
    
    
    
    
    

另外在媒體查詢中設置:

@media (prefers-color-scheme: dark) {
    :root {
        --background-color: #111416
        --text-color: #ccc;
        --link-color: #f96;
    }

    svg {
        color: var(--text-color)
    }
}

如果你分成多個文件的話,可能會像下面這樣的:

/* dark.css */
:root {
    --color: rgb(250, 250, 250);
    --background-color: rgb(5, 5, 5);
    --link-color: rgb(0, 188, 212);
    --main-headline-color: rgb(233, 30, 99);
    --accent-background-color: rgb(0, 188, 212);
    --accent-color: rgb(5, 5, 5);
}

/* light.css */
:root {
    --color: rgb(5, 5, 5);
    --background-color: rgb(250, 250, 250);
    --link-color: rgb(0, 0, 238);
    --main-headline-color: rgb(0, 0, 192);
    --accent-background-color: rgb(0, 0, 238);
    --accent-color: rgb(250, 250, 250);
}

/* style.css */
:root {
    color-scheme: light dark;
}

body {
    color: var(--color);
    background-color: var(--background-color);
}

svg {
    color: var(--color);
}

讓切換有一個過渡效果

熟悉CSS的同學都應該記得,CSS的transition可以讓元素在兩個狀態的切換過程中有一個平滑過渡的效果,以至於不會那么生硬:

如何給網站添加暗黑模式 20

而我們聊的dark/light兩模式之間的切換剛好穩合transition。加上dark/light兩模式之間的切換就是color和background-color屬性值的切換。為了讓整個切換過程有一個過渡效果,我們可以把transition加上來。比如:

body {
    --duration: 0.5s;
    --timing: ease;

    color: var(--color);
    background-color: var(--background-color);

    transition: color var(--duration) var(--timing), background-color var(--duration) var(--timing);
}

JavaScript實現dark/light模式切換

如果你不依任CSS,或者說希望讓自己的Web網站或Web應用程序都具備dark/light模式的切換,那麼可以通過JavaScript來實現。因為dark/light模式的切換說到底就是兩套主題的切換。當然,你可以讓該JS的能力更為強大一些,不僅僅是對.css文件的切換,粗暴簡單的實現網站換膚這樣的一個功能。或許你可以這樣做:

  • 在網站上提供相應的切換按鈕(比如一個tab選項卡,也可以是一個radio按鈕),方便用戶自行選擇
  • 該JS可以對系統級別做監聽,如果用戶從系統級別開啟了暗黑模式,那麼就把樣式文件切換到dark.css下
  • 還可以根據時間來做一個dark/light模式的切換,比如說白天採用light模式,晚上使用dark模式
  • 和CSS實現dark/light模式切換一樣,還可以在JS中加上transition效果,讓模式在切換的過程有一個過渡效果

為了節約篇幅,這裡就不把JavaScript代碼貼出來了,感興趣的話可以看看@Koos Looijesteijn的《 A guide to implementing dark modes on websites》【18】一文。文章中詳細的根據上面幾個過程,向大家展示了對應的JavaScript代碼。如果你不願閱讀文章的話,可以點擊鏈接直接閱讀源代碼【19】。 (https://www.kooslooijesteijn.net/blog/add-dark-mode-to-website

https://gist.github.com/kslstn/20f654fd27eb29619040c74fa6526919

不過就我個人而言,我不太推薦使用JavaScript方案,這可能和我自己的信條有關係:

在Web端能用CSS實現的絕不借助JavaScript。

瀏覽器配置

下面有一個簡單的示例:

Switch light / dark mode with prefers-color-scheme: [email protected]【20】(https://codepen.io/

在該示例的頁面上沒有提供任何切換按鈕給用戶做選擇。主要目的是希望頁面能根據系統級別的設置來決定採用什麼主題。假設你的系統默認就開啟了暗黑模式。但你在瀏覽器看到的效果也不一定是dark模式下的效果。這主要是因為瀏覽器對prefers-color-scheme支持有一定的差異。不過我們可以對瀏覽器做一些設置,讓頁面能正常的跟著系統設置做出正確的渲染。

如果你使用Firefox,可以在地址欄中輸入about:config,然後鼠標右鍵點擊選擇“新建(New)” → “整數(Integer)”,新建整數ui.systemUsesDarkTheme,並且將其值設置為1:

如何給網站添加暗黑模式 21

如果你使用的是Safari瀏覽器,可以使用它自帶的工具來查看效果:

如何給網站添加暗黑模式 22

最終在支持的瀏覽器下看到的效果如下:

如何給網站添加暗黑模式 23

除此之外,還可以給瀏覽器安裝插件。比如@CHRIS HOFFMAN在他的《 How to Enable Dark Mode for Google Chrome》【21】文章中就詳細的介紹了怎麼在Chrome瀏覽器安裝Dark Reader【22】插件,讓Web頁面具有暗黑模式瀏覽效果:(https://www.howtogeek.com/360650/how-to-enable-dark-mode-for-google-chrome/)(https://chrome.google.com/webstore/detail/dark-reader/eimadpbcbfnmbkopoojfekhnkhdbieeh

如何給網站添加暗黑模式 24

小結

上面我們通過不同的方式向大家闡述和演示瞭如何實現黑暗模式和高亮模式切換的解決方案。有粗暴簡單的方式,原始的切換樣式表的方式,還有採用一些新的CSS特性,比如CSS自定義屬性,新的媒體查詢特性,還有神奇的濾鏡和混合模式。而且這些解決方案中既有CSS和JavaScript的混合解決方案,也有純CSS的解決方案,甚至還有原生系統和瀏覽器通信的解決方案。還是那句老話,不管哪種解決方案或者技術手段,都有自己的利弊,沒有最好,只有最適合的使用場景。在實際使用的時候,應該具體問題具體分析。

本文轉載自公眾號淘系技術(ID:AlibabaMTT)。

原文鏈接

https://mp.weixin.qq.com/s/dxHAlvnMNxI9r0MXruZ8pA