本篇同步發表於我的 Hashnode 部落格:
Eva Chen | 網頁設計師下班後 (hashnode.dev)
↓ 今日學習重點 ↓
使用 Media Queries 的 prefers-color-scheme
搭配 CSS 變數設定預設的淺色/深色模式使用 HTML input checkbox 做淺色與深色模式的切換
關於顏色的設定方法、顏色的變數、混色的新方法,我們在前兩篇都說過了,關於顏色還有什麼需要了解的呢?那就是近幾年在軟體設計中常出現的:淺色與深色模式切換。
今天我們要學會使用 CSS Media Queries 加上 CSS 變數打造自己的淺色與深色模式,甚至更進一步,用 HTML 的 input checkbox 做到簡單的切換(toggle)行為。
另外,雖然 SASS (SCSS) 與 JS 不在本系列學習範圍內,但是還是提供了他們的解法給大家參考。
prefers-color-scheme
在 CSS 中我們可以透過 Media Queries 的 prefers-color-scheme
偵測使用者的裝置是偏好淺色還是深色模式。語法很簡單,總共有 3 個值可以使用:
no-preference
:當使用者裝置偏好色系不知道時
light
:當使用者裝置偏好淺色模式時
dark
:當使用者裝置偏好深色模式時
寫起來如下:
@media (prefers-color-scheme: no-preference) { ... }
@media (prefers-color-scheme: light) { ... }
@media (prefers-color-scheme: dark) { ... }
接著,我們可以使用 CSS 的變數,設定淺色模式時的 11 個黑白灰階層(由淺至深,含黑白色),然後,再另外於深色模式時將變數依序顛倒設定(由深至淺,含黑白色),覆蓋開頭設定的灰階變數。
關於 CSS 變數的的寫法,可參考這系列之前的文章:
#09 原生的 CSS 變數,基本與進階應用
CSS
/* -------- 預設 -------- */
:root {
--gray-000: #fff;
--gray-100: #f9f9fd;
--gray-200: #e8edf4;
--gray-300: #DBE2E7;
--gray-400: #CED7E0;
--gray-500: #A9B3BD;
--gray-600: #677889;
--gray-700: #495562;
--gray-800: #2D4155;
--gray-900: #1A2632;
--gray-1000: #000;
}
/* -------- 當使用者偏好深色模式 -------- */
@media (prefers-color-scheme: dark){
:root{
--gray-000: #000;
--gray-100: #1A2632;
--gray-200: #2D4155;
--gray-300: #495562;
--gray-400: #677889;
--gray-500: #A9B3BD;
--gray-600: #CED7E0;
--gray-700: #DBE2E7;
--gray-800: #e8edf4;
--gray-900: #f9f9fd;
--gray-1000: #fff;
}
}
補個設計小知識,以上範例的黑白灰,在色相上都帶有一點點的藍色。
因為如果是純正的黑白灰,沒有一點顏色的話,看起來會很像影印的結果,讓人感覺死氣沉沉。所以在 UI 設計上,常常見到黑白灰會加上一點藍色或紫色調,呈現結果會比較漂亮。當然這不是絕對,也是有很棒的純黑白設計。
這樣一來,只要需要切換顏色的地方都套用我們設定的黑白灰變數,就能夠依據使用者偏好色系,呈現對應的的淺色/深色模式囉!
DEMO 連結:@media prefers-color-scheme
不過,這樣還不夠,我們通常會希望:預設顏色模式是依據使用者偏好色系,但是使用者也可以自主切換淺色/深色模式。
其實 HTML 的 input checkbox 加上 CSS 的 :checked
偽類,能夠在不寫 JS 的情況下,做到簡單的來回切換(toggle)行為。
如果換頁時要記住使用者的設定,就需要使用到 JS,可以在 cookie 中存放一個布林值,然後在每一頁套用在這個 input 上。
HTML
<input type="checkbox" id="color-scheme-toggle">
<!-- 使用 label 控制 checkbox -->
<label for="color-scheme-toggle">
<span class="material-icons"></span>
</label>
CSS
/* 隱藏起 checkbox,並且防止誤觸 */
#color-scheme-toggle{
position: fixed;
pointer-events: none;
width: 0;
height: 0;
opacity: 0;
}
label[for="color-scheme-toggle"] {
/* 設定基本樣式 */
display: inline-block;
margin-top: 2rem;
padding: 1rem 1.5rem;
color: var(--gray-700);
cursor: pointer;
border: 1px solid currentcolor;
border-radius: 2rem;
transition: var(--transition-base);
/* 因為想要使用 CSS 切換文字,所以使用 ::before、::after 放文字 */
.material-icons::before{
content: "light_mode";
}
&::after{
content: "Switch to Light Theme";
}
}
接著,透過 checkbox 的勾選與否,加上使用父層選擇器 :has
,設定當有勾選的時候 html 變數不一樣,把 :root
預設的變數覆蓋過去,就能改變整頁的設定了:
注意:Firefox 桌機版即將在下個版本(版本 121)才支援
:has
,而 Firefox Android 尚未支援,開發時請斟酌一下用戶瀏覽器的支援程度。(2023/11)
/* -------- 預設 -------- */
:root {
/* 略:設定淺色變數與文字樣式 */
}
/* 當 html 中的 #color-scheme-toggle checked 時 */
html:has(#color-scheme-toggle:checked) {
/* 略:設定深色變數與文字樣式 */
}
/* -------- 當使用者偏好深色模式 -------- */
@media (prefers-color-scheme: dark) {
/* 略:設定深色變數與文字樣式 */
/* 當 html 中的 #color-scheme-toggle checked 時 */
html:has(#color-scheme-toggle:checked) {
/* 略:設定淺色變數與文字樣式 */
}
}
實際結果結果如下:
DEMO 連結:Pure CSS Color Scheme Toggle
SASS (SCSS) 不在本次系列文的教學範圍內,不會的可以先跳過。
剛剛那樣寫有一點髒髒的,我們重複寫了兩遍長長的淺色與深色的變數與文字樣式,如果你會使用 SASS (SCSS) 可以用 @mixin
和迴圈 @for
管理:
$grays: #fff, #f9f9fd, #e8edf4, #DBE2E7, #CED7E0, #A9B3BD, #677889, #495562, #2D4155, #1A2632, #000;
@mixin light-mode() {
body {
@for $i from 1 through length($grays) {
$color: nth($grays, $i);
--gray-#{$i - 1}00: #{$color};
}
}
label[for="color-scheme-toggle"]{
.material-icons::before{ content: "dark_mode"; }
&::after{ content: "Switch to Dark Theme"; }
}
}
@mixin dark-mode() {
body {
@for $i from 1 through length($grays) {
$color: nth($grays, $i);
--gray-#{10 - $i + 1}00: #{$color};
}
}
label[for="color-scheme-toggle"]{
.material-icons::before{ content: "light_mode"; }
&::after{ content: "Switch to Light Theme"; }
}
}
// -------- 預設 -------- //
@include light-mode();
html:has(#color-scheme-toggle:checked) {
@include dark-mode();
}
// -------- 當使用者偏好深色模式 -------- //
@media (prefers-color-scheme: dark) {
@include dark-mode();
html:has(#color-scheme-toggle:checked) {
@include light-mode();
}
}
JS 不在本次系列文的教學範圍內,不會的可以先跳過。
當然,我們也可以使用 JS 切換樣式。
這邊不管是淺色還是深色模式,CSS 預設的變數寫在 :root
裡,相反的變數則寫成 class,要切換時使用 JS 在 HTML body 加上對應的 class,把 :root
預設的變數覆蓋過去。
我本來使用 JS 切換,是想要減少重複寫 CSS 變數的次數,但是 @media
和其他選擇器不能並列寫在一起,所以還是得要多寫一次深色模式的變數。所以,說有優化其實也還好,就當多一種寫法給大家參考囉!如果你有更好的方式也歡迎底下留言告訴我。
(PS. 這邊的範例沒有 watch 系統預設偏好色系的改變,所以如果更換系統預設偏好色系,要重整才有作用)
CSS
/* -------- 預設 -------- */
:root, .light-mode { /* 略:設定淺色變數 */ }
/* -------- 當使用者偏好深色模式 -------- */
@media (prefers-color-scheme: dark) {
:root { /* 略:設定深色變數 */ }
}
.dark-mode { /* 略:設定深色變數 */ }
JS
const body = document.body;
const toggleIcon = document.getElementById("toggle-icon");
const toggleText = document.getElementById("toggle-text");
const iconString = ["light_mode", "dark_mode"];
const textString = ["Switch to Light Theme", "Switch to Dark Theme"];
// 套用初始文字
if (window.matchMedia('(prefers-color-scheme: dark)').matches){
toggleIcon.innerText = iconString[0];
toggleText.innerText = textString[0];
} else {
toggleIcon.innerText = iconString[1];
toggleText.innerText = textString[1];
}
function toggleString() {
toggleIcon.innerText = (toggleIcon.innerText === iconString[0]) ? iconString[1] : iconString[0];
toggleText.innerText = (toggleText.innerText === textString[0]) ? textString[1] : textString[0];
}
function toggleColorMode() {
if(window.matchMedia('(prefers-color-scheme: dark)').matches){
body.classList.toggle("light-mode");
} else {
body.classList.toggle("dark-mode");
}
toggleString();
}
DEMO 連結:Color Scheme Toggle
好的,關於 CSS 顏色的部分就暫時先告一個段落了。
接下來,我們要來準備處理圖片與畫圖形了。
如果你喜歡我的創作,還想看看其他有趣的分享與日常,
可以追蹤我的 IG @im1010ioio,或者是🧋送杯珍奶鼓勵我,謝謝你🥰。
To Eva Chen
我把你的範例複製貼上專案,在Microsoft Edge (a kind of web browser)執行,得到非預期的結果。 不管在甚麼preference color theme 按鈕永遠顯示以Switch to Dark mode
結尾的文字。
<!DOCTYPE html5>
<html>
<head>
<title>Color scheme toggle demo 2</title>
<link rel="stylesheet" href="./stylesheet.css"></style>
</head>
<body>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script src="toggle-color-scheme.js"></script>
<div class="grid-container resize">
<header>User Name</header>
<main>
<div class="msg">
<span>上午 12:52</span>
<p>Lorem, ipsum dolor.</p>
</div>
<div class="msg">
<span>上午 12:52</span>
<p>Lorem ipsum dolor sit, amet consectetur adipisicing elit. Laborum doloremque tempora molestias repellat, ad unde reprehenderit, sint cumque libero quam similique reiciendis aperiam id aliquam, tempore earum nemo sequi recusandae.</p>
</div>
<div class="msg">
<span>上午 12:52</span>
<p>Lorem ipsum dolor sit amet.</p>
</div>
<div class="msg">
<span>上午 12:52</span>
<p>Lorem </p>
</div>
<div class="msg">
<span>上午 12:52</span>
<p>Lorem, ipsum dolor.</p>
</div>
<div class="msg">
<span>上午 12:52</span>
<p>Lorem </p>
</div>
</main>
<footer>
<textarea placeholder="輸入訊息"></textarea>
</footer>
</div>
<button id="color-scheme-toggle" onclick="toggleColorMode()">
<span id="toggle-icon" class="material-icons">dark_mode</span>
<span id="toggle-text">Switch to Dark Theme</span>
</button>
</body>
</html>
// https://stackoverflow.com/questions/56393880/how-do-i-detect-dark-mode-using-javascript
const body = document.body;
const toggleIcon = document.getElementById("toggle-icon");
const toggleText = document.getElementById("toggle-text");
const iconString = ["light_mode", "dark_mode"];
const textString = ["Switch to Light Theme", "Switch to Dark Theme"];
// 套用初始文字
if (window.matchMedia('(prefers-color-scheme: dark)').matches){
toggleIcon.innerText = iconString[0];
toggleText.innerText = textString[0];
} else {
toggleIcon.innerText = iconString[1];
toggleText.innerText = textString[1];
}
function toggleString() {
toggleIcon.innerText = (toggleIcon.innerText === iconString[0]) ? iconString[1] : iconString[0];
toggleText.innerText = (toggleText.innerText === textString[0]) ? textString[1] : textString[0];
}
function toggleColorMode() {
if(window.matchMedia('(prefers-color-scheme: dark)').matches){
body.classList.toggle("light-mode");
} else {
body.classList.toggle("dark-mode");
}
toggleString();
}
VSC
Microsoft Edge
Hello, Code Lover,
這是因為你 JS 擺放的位置在 HTML 元素的前面,當瀏覽器解析 HTML 文件時,是從上到下依序執行,此時 DOM 元素還沒有完全載入,所以 JS 的 function 沒有辦法透過 id 找到 HTML DOM 元素。
(JS 會回傳 null
,而 null
不會有 innerText
,所以會出現錯誤「Cannot set properties of null (setting 'innerText')
」,如果你打開瀏覽器的 devtool 的話,就會在 console 面板中看到)
一般會建議將 JS 放在 body 結束標籤 </body>
前,先讓 HTML DOM 元素載完,再執行 JS。
另外,如果你想用 Vue 框架的話,可以直接用 Vue 的語法綁定 function 和變數,Code 會更簡潔喔!