Tailwind CSS 深色模式完整實踐指南

🌙 為什麼需要深色模式?

深色模式不僅僅是一個美觀的功能,它還能:

  • 減少眼睛疲勞,特別是在低光環境下
  • 延長 OLED 螢幕裝置的電池壽命
  • 提升使用者體驗和滿意度
  • 展現應用的專業性和現代感

🎨 Tailwind CSS 深色模式策略

Tailwind CSS 提供三種深色模式策略:

1. Media Query 策略

1
2
3
4
5
// tailwind.config.js
module.exports = {
darkMode: 'media', // 根據系統偏好
// ...
}

2. Class 策略(推薦)

1
2
3
4
5
// tailwind.config.js
module.exports = {
darkMode: 'class', // 手動控制
// ...
}

3. Selector 策略

1
2
3
4
5
// tailwind.config.js
module.exports = {
darkMode: ['selector', '[data-mode="dark"]'],
// ...
}

本文將聚焦於最靈活的 class 策略

🛠️ 實作步驟

步驟 1:配置 Tailwind

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// tailwind.config.js
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
darkMode: 'class',
theme: {
extend: {
colors: {
// 自訂顏色變數
primary: "#2b7cee",
"background-light": "#f8fafc",
"surface-light": "#ffffff",
// ...深色模式顏色會自動處理
},
},
},
plugins: [],
}

步驟 2:建立 Dark Mode Hook

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
32
33
34
35
36
37
// hooks/useDarkMode.ts
import { useEffect, useState } from 'react';

type Theme = 'light' | 'dark';

export function useDarkMode(): [Theme, (theme: Theme) => void] {
const [theme, setTheme] = useState<Theme>(() => {
// 1. 檢查 localStorage
const saved = localStorage.getItem('darkMode');
if (saved) {
return saved === 'true' ? 'dark' : 'light';
}

// 2. 檢查系統偏好
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
return 'dark';
}

// 3. 預設淺色
return 'light';
});

useEffect(() => {
const root = document.documentElement;

if (theme === 'dark') {
root.classList.add('dark');
} else {
root.classList.remove('dark');
}

// 儲存偏好
localStorage.setItem('darkMode', String(theme === 'dark'));
}, [theme]);

return [theme, setTheme];
}

步驟 3:建立切換按鈕元件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// components/DarkModeToggle.tsx
import { useDarkMode } from '../hooks/useDarkMode';

export function DarkModeToggle() {
const [theme, setTheme] = useDarkMode();

const toggleTheme = () => {
setTheme(theme === 'light' ? 'dark' : 'light');
};

return (
<button
onClick={toggleTheme}
className="p-2 rounded-lg bg-gray-100 dark:bg-gray-800
hover:bg-gray-200 dark:hover:bg-gray-700
transition-colors"
aria-label="Toggle Dark Mode"
>
<span className="material-symbols-outlined">
{theme === 'light' ? 'dark_mode' : 'light_mode'}
</span>
</button>
);
}

步驟 4:預防 Flash of Unstyled Content (FOUC)

在 HTML 的 <head> 中加入腳本,在頁面載入前設定主題:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!-- index.html -->
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">

<!-- 預防閃爍 -->
<script>
(function() {
const savedTheme = localStorage.getItem('darkMode');
if (savedTheme === 'true' ||
(!savedTheme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
}
})();
</script>

<title>My App</title>
</head>
<body>
<div id="root"></div>
</body>
</html>

🎯 實用樣式模式

基礎顏色切換

1
2
3
4
<div className="bg-white dark:bg-gray-900 
text-gray-900 dark:text-white">
內容
</div>

邊框和陰影

1
2
3
4
5
<div className="border border-gray-200 dark:border-gray-700
shadow-sm hover:shadow-md
dark:shadow-gray-900/50">
卡片內容
</div>

互動狀態

1
2
3
4
5
<button className="bg-blue-500 hover:bg-blue-600 
dark:bg-blue-600 dark:hover:bg-blue-700
text-white transition-colors">
按鈕
</button>

漸層背景

1
2
3
4
<div className="bg-gradient-to-br from-blue-50 to-indigo-100
dark:from-gray-900 dark:to-gray-800">
漸層背景區域
</div>

🔄 跨頁面/應用同步

如果你有多個網站或應用需要同步深色模式設定:

方法 1:使用 Storage Event

1
2
3
4
5
6
7
// 監聽其他分頁的變化
window.addEventListener('storage', (e) => {
if (e.key === 'darkMode') {
const isDark = e.newValue === 'true';
document.documentElement.classList.toggle('dark', isDark);
}
});

方法 2:使用相同的 localStorage Key

確保所有網站使用相同的 key 和網域:

1
2
// 統一的 Storage Key
const THEME_KEY = 'app-theme-preference';

📱 響應式設計考量

不同裝置上的深色模式體驗:

1
2
3
4
5
6
7
8
9
10
<div className="
// 手機:較暗的背景
bg-gray-900
// 平板以上:稍微亮一點
md:dark:bg-gray-800
// 桌面:更豐富的層次
lg:dark:bg-gradient-to-br lg:dark:from-gray-900 lg:dark:to-gray-800
">
響應式深色模式
</div>

🎨 設計最佳實踐

1. 對比度要足夠

1
2
3
4
5
// 好的對比度
text-gray-900 dark:text-gray-100

// 避免這樣(對比度不足)
text-gray-700 dark:text-gray-400

2. 使用語意化顏色

1
2
3
4
5
6
7
8
9
10
// tailwind.config.js
theme: {
extend: {
colors: {
'text-primary': 'rgb(15 23 42)', // slate-900
'text-secondary': 'rgb(100 116 139)', // slate-500
'surface': 'rgb(255 255 255)',
},
},
}

3. 圖片和媒體處理

1
2
3
4
5
<img 
src="/hero.jpg"
alt="Hero"
className="dark:opacity-80 dark:contrast-90"
/>

🧪 測試深色模式

瀏覽器 DevTools

  1. 開啟 Chrome DevTools
  2. Cmd/Ctrl + Shift + P
  3. 輸入 “Emulate CSS prefers-color-scheme”
  4. 選擇 dark 或 light

自動化測試

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// cypress/e2e/dark-mode.cy.js
describe('Dark Mode', () => {
it('應該能切換深色模式', () => {
cy.visit('/');

// 預設應該是淺色
cy.get('html').should('not.have.class', 'dark');

// 點擊切換按鈕
cy.get('[aria-label="Toggle Dark Mode"]').click();

// 應該變成深色
cy.get('html').should('have.class', 'dark');

// localStorage 應該已儲存
cy.window().then((win) => {
expect(win.localStorage.getItem('darkMode')).to.equal('true');
});
});
});

📊 效能考量

1. CSS 檔案大小

使用 class 策略會產生額外的 CSS,但 Tailwind 的 PurgeCSS 會移除未使用的樣式:

1
2
3
4
5
// tailwind.config.js
module.exports = {
content: ['./src/**/*.{js,jsx,ts,tsx}'],
// Tailwind 會自動移除未使用的深色模式 class
}

2. 重排和重繪

切換深色模式會觸發重排,優化方法:

1
2
3
4
5
6
7
8
9
10
// 使用 CSS 變數減少重排
:root {
--bg-color: #ffffff;
--text-color: #000000;
}

.dark {
--bg-color: #1a1a1a;
--text-color: #ffffff;
}

🔍 常見問題解決

Q1: 切換時出現閃爍

解決方案:確保在 <head> 中加入初始化腳本(見步驟 4)

Q2: 第三方元件不支援深色模式

解決方案:使用 CSS 變數包裝

1
2
3
4
.custom-component {
background: var(--bg-color);
color: var(--text-color);
}

Q3: 圖片在深色模式下太亮

解決方案:使用濾鏡調整

1
2
3
4
<img 
className="dark:brightness-90 dark:contrast-125"
src="/image.jpg"
/>

🚀 進階技巧

1. 系統偏好變化監聽

1
2
3
4
5
6
7
8
9
10
11
12
useEffect(() => {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');

const handleChange = (e: MediaQueryListEvent) => {
if (!localStorage.getItem('darkMode')) {
setTheme(e.matches ? 'dark' : 'light');
}
};

mediaQuery.addEventListener('change', handleChange);
return () => mediaQuery.removeEventListener('change', handleChange);
}, []);

2. 動畫過渡

1
2
3
4
5
6
/* globals.css */
* {
transition: background-color 0.3s ease,
border-color 0.3s ease,
color 0.3s ease;
}

📚 資源推薦

💭 總結

實現一個完整的深色模式系統需要考慮:

  1. ✅ 使用者偏好檢測和儲存
  2. ✅ 避免閃爍的初始化
  3. ✅ 語意化的顏色系統
  4. ✅ 良好的對比度和可讀性
  5. ✅ 跨平台和跨頁面同步

希望這篇文章能幫助你在專案中實現優雅的深色模式!

你的網站有實作深色模式嗎?遇到什麼挑戰?歡迎在下方留言討論!


相關文章

留言討論