
在現代網頁設計中,深色模式(Dark Mode / Dark Theme)已經是用戶體驗(UX)的基本標配。除了降低在暗處閱讀時的眼部疲勞,也能延長 OLED 螢幕的電池續航力。
本篇技術筆記以開源主題 Guntenbee 為例,完整記錄如何架構一個具備「系統主題偏好自動偵測 + Cookie 記憶 + 切換按鈕 + 防閃爍 (Anti-flicker)」的深色模式方案。同時針對日後「原作者主題升級」時如何以零成本(Git Patch / Child Theme)或極低 Token 的 AI 提示詞無痛重新套用進行深度解析。
一、核心需求與架構設計
一個健全的深色模式切換機制需要具備以下要點:
- 使用者優先權階層:
- 若使用者曾在站上點擊切換過,以 Cookie 儲存的偏好 為最優先。
- 若為首次造訪或未設定 Cookie,自動偵測使用者的系統外觀模式(
prefers-color-scheme: dark)。
- 避免白光閃爍(Anti-flicker / FOUC):
- 瀏覽器解析 HTML 時,如果等到完整腳本下載並在 DOMContentLoaded 才執行加 class,畫面上會出現短暫的白光閃爍。因此需在
<head>注入極輕量的行內(Inline)腳本提早設定 class。
- 瀏覽器解析 HTML 時,如果等到完整腳本下載並在 DOMContentLoaded 才執行加 class,畫面上會出現短暫的白光閃爍。因此需在
- 直覺易用的開關位置:
- 將切換開關置入 Header 頂部導覽列(
<div class="head-mast-inner">),確保各裝置與版型都能一致顯示。
- 將切換開關置入 Header 頂部導覽列(
二、功能實作步驟
1. 樣式實作:獨立樣式表 (css/dark-theme.css)
將所有深色模式樣式統一抽離至獨立檔案,避免直接污染原作者的 style.css,大幅降低後續維護與合併時的衝突風險。
/* ==========================================================================
Dark Theme Styles
========================================================================== */
/* 全域背景與文字 */
body.theme-dark {
background-color: #121417;
color: #c9d1d9;
}
body.theme-dark h1,
body.theme-dark h2,
body.theme-dark h3,
body.theme-dark h4,
body.theme-dark h5,
body.theme-dark h6 {
color: #f0f6fc;
}
/* 頂部導覽、底欄與行動端選單 */
body.theme-dark .head-mast,
body.theme-dark .head-outro,
body.theme-dark .navigation-mobile-wrap {
background-color: #181b20;
color: #c9d1d9;
}
/* 下拉選單 */
body.theme-dark .navigation-main ul {
background-color: #1a1e24;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.45);
border: 1px solid #2b3036;
}
/* 輸入框與表單元件 */
body.theme-dark input[type="text"],
body.theme-dark input[type="search"],
body.theme-dark select,
body.theme-dark textarea {
background-color: #1a1e24;
color: #e6edf3;
border-color: #343a42;
}
/* 主題切換按鈕與 SVG 圖示切換 */
.theme-switch-toggle {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border-radius: 50%;
background: transparent;
border: 0;
cursor: pointer;
color: inherit;
transition: color 0.18s ease, background-color 0.18s ease;
}
.theme-switch-icon svg {
width: 18px;
height: 18px;
fill: currentColor;
}
/* 淺色模式顯示月亮,深色模式顯示太陽 */
.theme-switch-icon .icon-sun { display: none; }
.theme-switch-icon .icon-moon { display: inline-block; }
body.theme-dark .theme-switch-icon .icon-sun { display: inline-block; }
body.theme-dark .theme-switch-icon .icon-moon { display: none; }
2. 行為邏輯:偵測與持久化 (js/theme-switch.js)
透過原生 JavaScript 實現無相依性(Vanilla JS)的偵測與 Cookie 儲存:
(function () {
'use strict';
var COOKIE_NAME = 'guntenbee_theme';
var COOKIE_DAYS = 365;
function getCookie(name) {
var match = document.cookie.match(new RegExp('(^|;\\s*)' + name + '=([^;]+)'));
return match ? decodeURIComponent(match[2]) : null;
}
function setCookie(name, value, days) {
var expires = '';
if (days) {
var date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
expires = '; expires=' + date.toUTCString();
}
document.cookie = name + '=' + encodeURIComponent(value) + expires + '; path=/; SameSite=Lax';
}
function getSystemTheme() {
return (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches)
? 'dark'
: 'light';
}
function getPreferredTheme() {
var savedTheme = getCookie(COOKIE_NAME);
return (savedTheme === 'dark' || savedTheme === 'light') ? savedTheme : getSystemTheme();
}
function applyTheme(theme) {
var isDark = (theme === 'dark');
document.body.classList.toggle('theme-dark', isDark);
document.body.classList.toggle('theme-light', !isDark);
document.documentElement.classList.toggle('theme-dark', isDark);
document.documentElement.classList.toggle('theme-light', !isDark);
var toggles = document.querySelectorAll('.theme-switch-toggle');
toggles.forEach(function (btn) {
btn.setAttribute('aria-pressed', isDark ? 'true' : 'false');
btn.setAttribute('title', isDark ? '切換至淺色模式' : '切換至深色模式');
});
}
function init() {
applyTheme(getPreferredTheme());
document.addEventListener('click', function (e) {
var toggle = e.target.closest('.theme-switch-toggle');
if (toggle) {
e.preventDefault();
var currentTheme = document.body.classList.contains('theme-dark') ? 'dark' : 'light';
var nextTheme = (currentTheme === 'dark') ? 'light' : 'dark';
setCookie(COOKIE_NAME, nextTheme, COOKIE_DAYS);
applyTheme(nextTheme);
}
});
// 監聽作業系統外觀模式動態變化
if (window.matchMedia) {
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function () {
if (!getCookie(COOKIE_NAME)) {
applyTheme(getSystemTheme());
}
});
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
3. WordPress 資源載入與防閃爍 (inc/scripts-styles.php)
在 wp_enqueue_scripts 鉤子中註冊資源,並加入極速 Inline 腳本:
// Enqueue 樣式與腳本
wp_register_style( 'gutenbee-dark-theme', get_theme_file_uri( '/css/dark-theme.css' ), array( 'gutenbee-style' ), gutenbee_asset_version() );
wp_enqueue_style( 'gutenbee-dark-theme' );
// 防閃爍腳本:在頁面剛開始解析 DOM 時搶先將 theme-dark 注入 <html>
$anti_flicker_js = "(function(){try{var m=document.cookie.match(/(^|;\\s*)guntenbee_theme=([^;]+)/);var t=m?decodeURIComponent(m[2]):null;var d=(window.matchMedia&&window.matchMedia('(prefers-color-scheme: dark)').matches)?'dark':'light';if((t||d)==='dark'){document.documentElement.classList.add('theme-dark');}}catch(e){}})();";
wp_add_inline_script( 'gutenbee-dependencies', $anti_flicker_js, 'before' );
wp_register_script( 'gutenbee-theme-switch', get_theme_file_uri( '/js/theme-switch.js' ), array(), gutenbee_asset_version(), true );
wp_enqueue_script( 'gutenbee-theme-switch' );
4. 範本標籤與按鈕輸出 (inc/template-tags.php & Header Templates)
- 定義輔助函數:
function gutenbee_the_theme_switch_button() { ?> <button type="button" class="theme-switch-toggle" aria-label="切換深淺模式"> <span class="theme-switch-icon" aria-hidden="true"> <svg class="icon-moon" viewBox="0 0 24 24"><path d="M12.3 2a10 10 0 0 0-.19 14 10 10 0 0 0 11.63 4A10 10 0 1 1 12.3 2z"/></svg> <svg class="icon-sun" viewBox="0 0 24 24"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><line x1="12" y1="21" x2="12" y2="23" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><line x1="1" y1="12" x2="3" y2="12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><line x1="21" y1="12" x2="23" y2="12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg> </span> </button> <?php } - 在
template-parts/header/header-full.php、header-split.php、header-navbar.php的<div class="head-mast-inner">內部直接呼叫gutenbee_the_theme_switch_button()即可完美呈現。
三、未來原作者升級主題時的處理策略
當原主題作者發布新版本更新時,若直接覆蓋整個主題資料夾,客製化代碼將會遺失。以下提供三種不同維護情境的應對方式:
策略 A:建立子主題(Child Theme)—— 永遠不被覆蓋(推薦最佳實踐)
WordPress 的標準升級防禦機制是子主題。只需將新寫好的 dark-theme.css、theme-switch.js 及對應 template parts 放進子主題資料夾,日後父主題無論升級幾次,客製化邏輯都不會被覆蓋。
策略 B:利用 Git Patch 自動打補丁(零 Token 快速升級)
若習慣直接於主題目錄工作,建議在功能完成時立即產生 patch 檔案備份:
# 產出補丁檔
git diff > dark-theme.patch
當日後原作者有新版發布時:
# 下載並解壓縮覆蓋原主題後,執行一鍵套用
git apply --reject --whitespace=fix dark-theme.patch
git apply 具備自動比對行號上下文與建立新檔案的能力,速度快且完全不花費任何 AI token。
策略 C:低 Token 的精準 AI 提示詞(AI Prompt Template)
若在遠端或未配備 Git 的環境,需請 AI 助理重新在原作者新版主題植入 Dark Theme 功能時,千萬不要使用模糊指令(例如「幫我在這個主題加深色模式」,這會讓 AI 花費數萬 Token 去探索全文結構與測試選擇器)。
直接複製使用以下經結構化優化的 精準 Prompt:
請為 @guntenbee 主題實作 Dark Theme 功能。請直接依照以下規格建立檔案與注入代碼,不需漫遊搜尋其他檔案:
1. 新增樣式與腳本:
- 建立 `css/dark-theme.css`:定義 `body.theme-dark` 樣式(深色底色 #121417、文字 #c9d1d9、容器/選單 #181b20)與 `.theme-switch-toggle` 按鈕 SVG 切換動態。
- 建立 `js/theme-switch.js`:讀取/寫入 Cookie `guntenbee_theme`(效期 365 天);若無 Cookie 則以 `prefers-color-scheme: dark` 為準,並監聽 OS 主題變化與按鈕點擊事件。
2. 註冊與防閃爍(修改 `inc/scripts-styles.php`):
- 在 `gutenbee_enqueue_scripts` 中以 `wp_enqueue_style` 載入 `dark-theme.css`。
- 注入 Inline Script 到 `<head>`:若 Cookie 或系統偏好為 dark,即時加上 `document.documentElement.classList.add('theme-dark')` 以防止白光閃爍。
- 以 `wp_enqueue_script` 載入 `theme-switch.js`。
3. 按鈕與鉤子整合:
- 在 `inc/template-tags.php` 新增 `gutenbee_the_theme_switch_button()` 輸出太陽/月亮按鈕 HTML。
- 在 `template-parts/header/header-*.php` 各樣板的 `<div class="head-mast-inner">` 的 `head-content-slot` 中呼叫該按鈕函式。
- 在 `inc/default-hooks.php` 的 `gutenbee_body_class` 過濾器中檢查 `$_COOKIE['guntenbee_theme']`,有 dark 值即附加 `theme-dark` class。
為什麼這個 Prompt 具備高性價比(Low Token Usage)?
- 消除探勘步驟:精確指出檔案名稱(如
inc/scripts-styles.php、template-parts/header/header-*.php),AI 不需調用目錄搜尋工具消耗大量 Context Window。 - 精準函式命名:直接給予目標鉤子(
gutenbee_body_class、gutenbee_enqueue_scripts),AI 思考路徑縮短,輸出 token 減少 70% 以上。 - 成果可預測且易於維護:模組化規範使每一次生成的樣式結構一致,日後維護與問題排查極為單純。