
本篇同步發表於我的 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 會更簡潔喔!