基礎概念簡介:如何建構可適應性強且無障礙的主題切換元件。
在這篇文章中,我想分享如何建構深色和淺色主題切換元件的思考過程。 立即試用。
如果比較喜歡看影片,可以觀看這篇貼文的 YouTube 版本:
總覽
網站可能會提供色彩配置控制設定,而非完全依賴系統偏好設定。也就是說,使用者可能會以系統偏好設定以外的模式瀏覽網頁。舉例來說,使用者的系統採用淺色主題,但使用者偏好網站顯示深色主題。
建構這項功能時,需要考量多項網頁工程因素。舉例來說,瀏覽器應盡快瞭解偏好設定,避免網頁顏色閃爍,且控制項必須先與系統同步,然後允許用戶端儲存例外狀況。
標記
切換按鈕應使用 <button>,因為這樣就能享有瀏覽器提供的互動事件和功能,例如點擊事件和焦點功能。
按鈕
按鈕需要類別,才能透過 CSS 使用;需要 ID,才能透過 JavaScript 使用。
此外,由於按鈕內容是圖示而非文字,請新增 title 屬性,提供按鈕用途的相關資訊。最後,請新增 [aria-label],保留圖示按鈕的狀態,讓螢幕閱讀器向視障人士分享主題狀態。
<button
class="theme-toggle"
id="theme-toggle"
title="Toggles light & dark"
aria-label="auto"
>
…
</button>
aria-label和aria-live禮貌
如要向螢幕閱讀器指出應播報 aria-label 的變更,請在按鈕中加入 aria-live="polite"。
<button
class="theme-toggle"
id="theme-toggle"
title="Toggles light & dark"
aria-label="auto"
aria-live="polite"
>
…
</button>
新增這項標記後,螢幕閱讀器就會以禮貌的方式 (而非 aria-live="assertive") 告知使用者變更內容。以這個按鈕為例,系統會根據 aria-label 的狀態,宣布「淺色」或「深色」。
可擴充向量圖形 (SVG) 圖示
SVG 可讓您以最少的標記建立高品質的可縮放圖形。與按鈕互動可以觸發向量的新視覺狀態,因此 SVG 非常適合用於圖示。
下列 SVG 標記會放在 <button> 內:
<svg class="sun-and-moon" aria-hidden="true" width="24" height="24" viewBox="0 0 24 24">
…
</svg>
aria-hidden 已新增至 SVG 元素,因此螢幕閱讀器會忽略該元素,因為該元素已標示為呈現元素。這很適合用於視覺裝飾,例如按鈕內的圖示。除了元素上必要的 viewBox 屬性外,請新增高度和寬度,原因與圖片應取得內嵌大小類似。
太陽
![]()
太陽圖案由圓圈和線條組成,SVG 恰好有這些形狀。將 cx 和 cy 屬性設為 12,即可將 <circle> 設為中心,這是可視區域大小 (24) 的一半,然後指定半徑 (r) 為 6,即可設定大小。
<svg class="sun-and-moon" aria-hidden="true" width="24" height="24" viewBox="0 0 24 24">
<circle class="sun" cx="12" cy="12" r="6" mask="url(#moon-mask)" fill="currentColor" />
</svg>
此外,遮罩屬性會指向 SVG 元素的 ID,您接下來會建立該 ID,最後會使用 currentColor 提供與網頁文字顏色相符的填滿顏色。
陽光
![]()
接著,在圓圈正下方,於群組
元素 <g>
中新增光束線。
<svg class="sun-and-moon" aria-hidden="true" width="24" height="24" viewBox="0 0 24 24">
<circle class="sun" cx="12" cy="12" r="6" mask="url(#moon-mask)" fill="currentColor" />
<g class="sun-beams" stroke="currentColor">
<line x1="12" y1="1" x2="12" y2="3" />
<line x1="12" y1="21" x2="12" y2="23" />
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
<line x1="1" y1="12" x2="3" y2="12" />
<line x1="21" y1="12" x2="23" y2="12" />
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
</g>
</svg>
這次不是設定「填滿」的值,而是設定每條線的「筆觸」。currentColor線條加上圓形,即可繪製出光芒四射的太陽。
月球
為了營造光線 (太陽) 和黑暗 (月亮) 之間無縫轉換的錯覺,月亮是太陽圖示的擴增版本,使用 SVG 遮罩。
<svg class="sun-and-moon" aria-hidden="true" width="24" height="24" viewBox="0 0 24 24">
<circle class="sun" cx="12" cy="12" r="6" mask="url(#moon-mask)" fill="currentColor" />
<g class="sun-beams" stroke="currentColor">
…
</g>
<mask class="moon" id="moon-mask">
<rect x="0" y="0" width="100%" height="100%" fill="white" />
<circle cx="24" cy="10" r="6" fill="black" />
</mask>
</svg>
SVG 遮罩功能強大,可讓白色和黑色移除或納入其他圖像的部分。只要將圓形移入和移出遮罩區域,太陽圖示就會被 SVG 遮罩的月亮<circle>形狀遮住。
如果 CSS 未載入會發生什麼情況?
測試 SVG 時,可以模擬 CSS 未載入的情況,確保結果不會過大或導致版面配置問題。SVG 上的內嵌高度和寬度屬性,加上 currentColor 的使用,可為瀏覽器提供最少的樣式規則,以便在 CSS 未載入時使用。這可針對網路不穩定的情況提供良好的防禦風格。
版面配置
主題切換元件的表面積很小,因此不需要使用格線或彈性方塊進行版面配置。而是使用 SVG 定位和 CSS 轉換。
樣式
.theme-toggle 種樣式
<button> 元素是圖示形狀和樣式的容器。這個父項內容會保留要傳遞至 SVG 的自適應顏色和大小。
第一項工作是將按鈕設為圓形,並移除預設按鈕樣式:
.theme-toggle {
--size: 2rem;
background: none;
border: none;
padding: 0;
inline-size: var(--size);
block-size: var(--size);
aspect-ratio: 1;
border-radius: 50%;
}
接著,新增一些互動樣式。為滑鼠使用者新增游標樣式。新增 touch-action: manipulation,即可快速回應觸控操作。移除 iOS 套用至按鈕的半透明醒目顯示效果。最後,請為焦點狀態外框與元素邊緣之間預留一些空間:
.theme-toggle {
--size: 2rem;
background: none;
border: none;
padding: 0;
inline-size: var(--size);
block-size: var(--size);
aspect-ratio: 1;
border-radius: 50%;
cursor: pointer;
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
outline-offset: 5px;
}
按鈕內的 SVG 也需要一些樣式。SVG 應符合按鈕大小,並將線條結尾設為圓角,以呈現柔和的視覺效果:
.theme-toggle {
--size: 2rem;
background: none;
border: none;
padding: 0;
inline-size: var(--size);
block-size: var(--size);
aspect-ratio: 1;
border-radius: 50%;
cursor: pointer;
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
outline-offset: 5px;
& > svg {
inline-size: 100%;
block-size: 100%;
stroke-linecap: round;
}
}
使用 hover 媒體查詢功能調整大小
圖示按鈕大小為 2rem,對滑鼠使用者來說還算合適,但對手指等粗略指標來說可能有點小。使用懸停媒體查詢指定大小增量,讓按鈕符合許多觸控大小規範。
.theme-toggle {
--size: 2rem;
…
@media (hover: none) {
--size: 48px;
}
}
太陽和月亮 SVG 樣式
按鈕會保留主題切換元件的互動式層面,而內部的 SVG 則會保留視覺和動畫層面。您可以在這裡美化圖示,讓圖示栩栩如生。
淺色主題
如要讓縮放和旋轉動畫從 SVG 形狀的中心開始,請設定 transform-origin: center center。這裡的形狀會使用按鈕提供的自動調整顏色。月亮和太陽使用按鈕提供的 var(--icon-fill) 和 var(--icon-fill-hover) 填滿,而陽光則使用變數筆觸。
.sun-and-moon {
& > :is(.moon, .sun, .sun-beams) {
transform-origin: center center;
}
& > :is(.moon, .sun) {
fill: var(--icon-fill);
@nest .theme-toggle:is(:hover, :focus-visible) > & {
fill: var(--icon-fill-hover);
}
}
& > .sun-beams {
stroke: var(--icon-fill);
stroke-width: 2px;
@nest .theme-toggle:is(:hover, :focus-visible) & {
stroke: var(--icon-fill-hover);
}
}
}
深色主題
月亮樣式需要移除光束、放大太陽圓圈,並移動圓圈遮罩。
.sun-and-moon {
@nest [data-theme="dark"] & {
& > .sun {
transform: scale(1.75);
}
& > .sun-beams {
opacity: 0;
}
& > .moon > circle {
transform: translateX(-7px);
@supports (cx: 1px) {
transform: translateX(0);
cx: 17px;
}
}
}
}
請注意,深色主題沒有顏色變化或轉場效果。父項按鈕元件擁有這些顏色,且這些顏色已在深色和淺色情境中進行調整。轉場資訊應位於使用者的動態偏好設定媒體查詢後方。
動畫
此時按鈕應可運作並具備狀態,但沒有轉場效果。以下各節將說明如何定義如何和什麼轉場效果。
分享媒體查詢和匯入緩和效果
為方便您根據使用者的作業系統動作偏好設定,將轉場效果和動畫放在後方,PostCSS 外掛程式 Custom Media 可讓您使用媒體查詢變數的 CSS 規格草案語法:
@custom-media --motionOK (prefers-reduced-motion: no-preference);
/* usage example */
@media (--motionOK) {
.sun {
transition: transform .5s var(--ease-elastic-3);
}
}
如要使用獨特且簡單易用的 CSS 緩和效果,請匯入 Open Props 的 easings 部分:
@import "https://unpkg.com/open-props/easings.min.css";
/* usage example */
.sun {
transition: transform .5s var(--ease-elastic-3);
}
太陽
太陽的轉場效果會比月亮更活潑,並透過彈性緩和效果達成此目的。光束應在旋轉時少量彈跳,太陽中心應在縮放時少量彈跳。
預設 (淺色主題) 樣式會定義轉場效果,而深色主題樣式則會定義轉場至淺色主題的自訂項目:
.sun-and-moon {
@media (--motionOK) {
& > .sun {
transition: transform .5s var(--ease-elastic-3);
}
& > .sun-beams {
transition:
transform .5s var(--ease-elastic-4),
opacity .5s var(--ease-3)
;
}
@nest [data-theme="dark"] & {
& > .sun {
transform: scale(1.75);
transition-timing-function: var(--ease-3);
transition-duration: .25s;
}
& > .sun-beams {
transform: rotateZ(-25deg);
transition-duration: .15s;
}
}
}
}
在 Chrome 開發人員工具的「動畫」面板中,您可以找到動畫轉場效果的時間軸。您可以檢查動畫總時間長度、元素和緩和時間。
月球
月亮的光亮和黑暗位置已設定完成,請在 --motionOK 媒體查詢中加入轉場效果樣式,讓月亮動起來,同時尊重使用者的動態偏好設定。
延遲時間和持續時間是順利轉換的關鍵。 如果太陽太早被遮蔽,例如轉場效果不像是經過精心設計或充滿趣味,而是顯得混亂。
.sun-and-moon {
@media (--motionOK) {
& .moon > circle {
transform: translateX(-7px);
transition: transform .25s var(--ease-out-5);
@supports (cx: 1px) {
transform: translateX(0);
cx: 17px;
transition: cx .25s var(--ease-out-5);
}
}
@nest [data-theme="dark"] & {
& > .moon > circle {
transition-delay: .25s;
transition-duration: .5s;
}
}
}
}
偏好減少動態效果
在大多數 GUI 挑戰中,我會保留一些動畫 (例如不透明度交叉淡出),供偏好減少動作的使用者使用。不過,這個元件的即時狀態變更效果更好。
JavaScript
這個元件中的 JavaScript 負責許多工作,包括管理螢幕閱讀器的 ARIA 資訊,以及從本機儲存空間取得及設定值。
網頁載入體驗
網頁載入時不得出現任何顏色閃爍。如果使用者採用深色色彩配置,但表示偏好使用這個元件的淺色模式,然後重新載入網頁,網頁一開始會顯示深色模式,接著閃爍一下,變成淺色模式。為避免這種情況,我們必須執行少量封鎖 JavaScript,盡可能及早設定 HTML 屬性 data-theme。
<script src="./theme-toggle.js"></script>
為此,系統會先載入文件中的純 <script> 標記 <head>,再載入任何 CSS 或 <body> 標記。瀏覽器遇到這類未標記的指令碼時,會執行程式碼,然後再執行其餘的 HTML。請謹慎使用這個封鎖時刻,因為您可以在主要 CSS 繪製網頁之前設定 HTML 屬性,避免出現閃爍或顏色。
JavaScript 會先檢查本機儲存空間中的使用者偏好設定,如果儲存空間中沒有任何設定,則會改為檢查系統偏好設定:
const storageKey = 'theme-preference'
const getColorPreference = () => {
if (localStorage.getItem(storageKey))
return localStorage.getItem(storageKey)
else
return window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light'
}
接下來會剖析在本機儲存空間中設定使用者偏好設定的函式:
const setPreference = () => {
localStorage.setItem(storageKey, theme.value)
reflectPreference()
}
接著是使用偏好設定修改文件的函式。
const reflectPreference = () => {
document.firstElementChild
.setAttribute('data-theme', theme.value)
document
.querySelector('#theme-toggle')
?.setAttribute('aria-label', theme.value)
}
此時請注意 HTML 文件剖析狀態。由於 <head> 標記尚未完全剖析,瀏覽器還不知道「#theme-toggle」按鈕。不過,瀏覽器確實有 document.firstElementChild,也就是 <html> 標記。函式會嘗試同時設定這兩者,以保持同步,但首次執行時只能設定 HTML 標記。一開始 querySelector 不會找到任何項目,而選用鏈結運算子可確保在找不到項目並嘗試叫用 setAttribute 函式時,不會發生語法錯誤。
接著,系統會立即呼叫該函式 reflectPreference(),因此 HTML 文件會設定 data-theme 屬性:
reflectPreference()
按鈕仍需要屬性,因此請等待網頁載入事件,然後即可安全地查詢、新增監聽器,並在下列項目上設定屬性:
window.onload = () => {
// set on load so screen readers can get the latest value on the button
reflectPreference()
// now this script can find and listen for clicks on the control
document
.querySelector('#theme-toggle')
.addEventListener('click', onClick)
}
切換體驗
按一下按鈕時,主題必須在 JavaScript 記憶體和文件中交換。您需要檢查目前的主題值,並決定新的狀態。設定新狀態後,請儲存並更新文件:
const onClick = () => {
theme.value = theme.value === 'light'
? 'dark'
: 'light'
setPreference()
}
與系統同步
這個主題切換鈕的獨特之處在於,它會隨著系統偏好設定變更而同步更新。如果使用者在網頁和這個元件顯示時變更系統偏好設定,主題切換按鈕會隨之變更,以符合新的使用者偏好設定,就像使用者在系統切換主題的同時與主題切換按鈕互動一樣。
如要達成這個目標,請使用 JavaScript 和 matchMedia 事件,監聽媒體查詢的變更:
window
.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', ({matches:isDark}) => {
theme.value = isDark ? 'dark' : 'light'
setPreference()
})
結論
現在您已瞭解我的做法,您會怎麼做呢?🙂
讓我們多元化方法,學習在網路上建構內容的所有方式。 建立試聽版,然後在推特上傳送連結給我,我會將連結加到下方的社群混音區!
社群重混作品
- @NathanG 在 Codepen 上使用 Vue
- @ShadowShahriar 在 Codepen 上發布的內容
- @tomayac 做為自訂元素
- @bramus 搭配原生 JavaScript
- @JoshWComeau,使用 React