🌙 為什麼需要深色模式?
深色模式不僅僅是一個美觀的功能,它還能:
- 減少眼睛疲勞,特別是在低光環境下
- 延長 OLED 螢幕裝置的電池壽命
- 提升使用者體驗和滿意度
- 展現應用的專業性和現代感
🎨 Tailwind CSS 深色模式策略
Tailwind CSS 提供三種深色模式策略:
1 2 3 4 5
| module.exports = { darkMode: 'media', }
|
2. Class 策略(推薦)
1 2 3 4 5
| module.exports = { darkMode: 'class', }
|
3. Selector 策略
1 2 3 4 5
| 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
|
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
| import { useEffect, useState } from 'react';
type Theme = 'light' | 'dark';
export function useDarkMode(): [Theme, (theme: Theme) => void] { const [theme, setTheme] = useState<Theme>(() => { const saved = localStorage.getItem('darkMode'); if (saved) { return saved === 'true' ? 'dark' : 'light'; } if (window.matchMedia('(prefers-color-scheme: dark)').matches) { return 'dark'; } 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
| 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
| <!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
| 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
| theme: { extend: { colors: { 'text-primary': 'rgb(15 23 42)', 'text-secondary': 'rgb(100 116 139)', 'surface': 'rgb(255 255 255)', }, }, }
|
3. 圖片和媒體處理
1 2 3 4 5
| <img src="/hero.jpg" alt="Hero" className="dark:opacity-80 dark:contrast-90" />
|
🧪 測試深色模式
- 開啟 Chrome DevTools
- 按
Cmd/Ctrl + Shift + P
- 輸入 “Emulate CSS prefers-color-scheme”
- 選擇 dark 或 light
自動化測試
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| 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'); cy.window().then((win) => { expect(win.localStorage.getItem('darkMode')).to.equal('true'); }); }); });
|
📊 效能考量
1. CSS 檔案大小
使用 class 策略會產生額外的 CSS,但 Tailwind 的 PurgeCSS 會移除未使用的樣式:
1 2 3 4 5
| module.exports = { content: ['./src/**/*.{js,jsx,ts,tsx}'], }
|
2. 重排和重繪
切換深色模式會觸發重排,優化方法:
1 2 3 4 5 6 7 8 9 10
| :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
| * { transition: background-color 0.3s ease, border-color 0.3s ease, color 0.3s ease; }
|
📚 資源推薦
💭 總結
實現一個完整的深色模式系統需要考慮:
- ✅ 使用者偏好檢測和儲存
- ✅ 避免閃爍的初始化
- ✅ 語意化的顏色系統
- ✅ 良好的對比度和可讀性
- ✅ 跨平台和跨頁面同步
希望這篇文章能幫助你在專案中實現優雅的深色模式!
你的網站有實作深色模式嗎?遇到什麼挑戰?歡迎在下方留言討論!
相關文章: