前面這幾天做了拖曳、排序動畫、滾動進場、視差,每一個都很吸睛。但是它們有個共同的前提:使用者看得到畫面、手上有滑鼠、也不介意畫面一直在動。只要其中一項不成立,這些效果就可能變成反效果。
其實我自己還沒針對無障礙優化過專案,但無論是前端熱門套件,還是 AI agent 寫 code 都能看到無障礙設計。所以今天來具體了解一下無障礙網頁到底是什麼,並整理出前端需要注意的重點。文章分成三段:先搞懂什麼是無障礙,再看 HTML 怎麼做,最後是 CSS 與 JavaScript 該注意什麼。那麼我們開始吧!
依照 MDN 的說法,無障礙網頁就是讓盡可能多的人都能使用網站,不論能力或環境如何。實際範圍比想像中大,不只侷限於身心障礙者,連老化、暫時性的受傷(手臂骨折用不了滑鼠)、情境限制(太陽底下看手機、網速很慢)都算在內。
MDN 又將這些使用者分成四類:
| 類型 | 常見輔助科技 | 我們要做的 |
|---|---|---|
| 視覺 | 螢幕閱讀器(NVDA、VoiceOver、TalkBack)、螢幕放大 | 替代文字、語意化 HTML、可縮放的版面 |
| 聽覺 | - | 影音提供字幕與文字稿 |
| 行動/肢體 | 鍵盤、其他替代輸入裝置 | 所有功能都能用鍵盤操作 |
| 認知 | - | 版面一致、流程簡單、清楚的錯誤訊息 |
認知類這方面,最近剛好深有感觸。長輩有個帳戶要確認是否本人使用,只需要填寫簡單的基本資料、送出就可以了。以長輩的 3C 能力來說綽綽有餘,但是她自己試了一個下午都不行。最後我一看,原來是前兩步的確認按鈕在左,取消按鈕在右;最後一步的確認按鈕卻反過來在右。再加上兩顆按鈕顏色一樣、按到取消也沒有反饋,一不小心就陷入無限確認取消地獄。
那要做到什麼程度算無障礙?目前最主要的國際標準是 W3C 發布的 WCAG(Web Content Accessibility Guidelines)。
目前最新的版本是 WCAG 2.2,2023 年 10 月成為 W3C Recommendation,在 2.1 的基礎上新增了 9 項成功準則,並移除了 4.1.1 Parsing。其中與系列文章中做過的東西最有關的有三項(皆為 AA):
MDN 也一再強調,無障礙要及早納入專案,並當成一般功能來測試。不用做到 100%,但要盡力提高覆蓋率。
💡 台灣的無障礙規範:台灣的官方入口是數位發展部的無障礙網路空間服務網,提供規範、檢測工具與標章申請。可以參考官網的「網站無障礙規範」。
螢幕閱讀器讀的並不是畫面,而是瀏覽器根據 DOM 產生的無障礙樹(accessibility tree)。每個節點都有 role(是什麼)、name(叫什麼)、state(現在什麼狀態)。所以不用特別去設定,語意化 HTML 做對了,無障礙樹就會跟著對。可以開啟 Chrome DevTools 的 Elements 面板就有 Accessibility 分頁可以查看無障礙樹。

MDN 把它叫做 POSH(Plain Old Semantic HTML):正確的元素做正確的工作。最經典的對照就是按鈕:
<!-- 😢 要自己補 tabindex、role、Enter/Space 事件、各種狀態樣式 -->
<div class="btn" @click="save">儲存</div>
<!-- 😊 鍵盤聚焦、Enter/Space 觸發、螢幕閱讀器角色,全都內建 -->
<button type="button" @click="save">儲存</button>
💡 ARIA 補充:
- 什麼是 ARIA? WAI-ARIA 是 W3C 制定的一組 HTML 屬性,用來替元素補上「是什麼(
role)」、「叫什麼(aria-label)」、「現在什麼狀態(aria-expanded)」,讓螢幕閱讀器聽得懂。例如只有圖示的按鈕,可以加上aria-label="關閉"。- 使用 ARIA 的第一守則:能用原生元素,就不要用 ARIA。ARIA 只補語意,不會補行為,它不會幫你加上鍵盤操作。(出處:Using ARIA | W3C)
另外,一般狀況 <a href> 是用來換頁的,<button> 是用來執行動作的,所以盡量不要寫成 <a href="#" onclick="...">。
h1~h6 是結構,不是字體大小。螢幕閱讀器可以用標題快速跳讀,不要為了想要的字級去挑標題層級。header、nav、main、aside、footer 讓使用者能直接跳到想去的區塊,一頁只放一個 main。order、grid-area 重排只改變視覺,不會改變閱讀和 Tab 的順序。<body>
<a class="skip-link" href="#main">跳到主要內容</a>
<header>…導覽列…</header>
<main id="main">…</main>
</body>
alt 要描述「這張圖在這個脈絡下傳達了什麼」。例如同一張狗狗照片,在留言區寫名字就好,若在領養頁面則要寫毛色、體型。裝飾用的圖片則寫成空的 alt="",讓螢幕閱讀器直接跳過。另外,不要把文字做進圖片裡,因為會讀不到,也無法選取複製。
像是之前做的 SVG 圖示,也是一樣的邏輯:
<!-- 有意義的圖示:給它名字 -->
<svg role="img" aria-label="已完成">…</svg>
<!-- 純裝飾:叫螢幕閱讀器跳過 -->
<svg aria-hidden="true" focusable="false">…</svg>
<label for>,placeholder 不能取代 label。<th scope> 標明是 col 還是 row;用 <caption> 放表格的標題或摘要。<label for="name">姓名</label>
<input id="name" name="name" type="text" />
<a href="report.pdf">下載 2026 年度報告(PDF)</a>
但如果設計稿上的連結文字就只有「下載」,也有辦法讓螢幕閱讀器念出完整名稱。有以下兩種做法:
<!-- 做法一:視覺隱藏文字,畫面上只看到「下載」 -->
<a href="report.pdf">
下載<span class="visually-hidden">2026 年度報告(PDF)</span>
</a>
<!-- 做法二:卡片或列表內,用 aria-labelledby 指向標題 -->
<article>
<h3 id="r1">2026 年度報告</h3>
<a href="report.pdf" id="d1" aria-labelledby="d1 r1">下載</a>
</article>
<html> 設定語言,例如 <html lang="zh-Hant-TW">,讓螢幕閱讀器用對的發音。aria-label 或隱藏的文字)。HTML 打好地基之後,CSS 和 JS 該做的事是「不要破壞它」,還有補上互動的部分。
1. 對比
WCAG AA 要求一般文字與背景的顏色對比至少 4.5:1(大字 3:1),UI 元件的邊框、圖示等則是 3:1。另外不要只靠顏色傳達資訊,例如錯誤欄位除了變紅色,還要有圖示或文字說明。如果有做深淺色主題,兩套主題都要檢查。比較簡單的檢查方式是用 DevTools 的對比顯示,點擊顏色旁的色塊後再點 Contrast ratio,就會顯示有沒有達到 AA 或 AAA 的標準。

2. 焦點樣式不要拿掉
outline: none 是很容易踩到的雷,拿掉 outline 之後鍵盤使用者就不知道自己在哪裡了。如果需要客製化樣式,就用 :focus-visible:
:focus-visible {
outline: 3px solid var(--focus-color);
outline-offset: 2px;
}
/* 對應 WCAG 2.4.11:被固定 header 遮住的元素,捲動時留出空間 */
html {
scroll-padding-top: 80px;
}
3. 隱藏內容要看對象
display: none 和 visibility: hidden 會讓視覺與螢幕閱讀器都看不到。display: none。.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
4. 支援減少動態設定
有些使用者對畫面移動很敏感,會在系統設定開啟「減少動態效果」。CSS 可以用 prefers-reduced-motion 讀到這個設定。前面做的動畫、滾動進場、視差,最容易引起不適的就是大面積的位移和視差,需要優先處理,進場動畫可以改成直接顯示:
@media (prefers-reduced-motion: reduce) {
.parallax,
.fade-in {
animation: none;
transition: none;
}
}
JS 也能讀到 prefers-reduced-motion 設定,偵測到的話,同樣讓進場動畫直接顯示就好:
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
💡 WCAG 對「互動觸發的動畫可以關閉」的要求(2.3.3)是 AAA 等級,不是 AA。所以這比較像是優化對動態敏感使用者的體驗,而不是通過 AA 的必要條件。
其他還有:
rem,而不是寫死的 px,可以讓使用者調整瀏覽器的預設字級時也跟著變。說個鬼故事,之前遇過一個設計稿,行動版越切越不對勁,後來發現行動版的元素尺寸其實是直接把桌機版的尺寸除以二。不只元素真滴有夠小,也因為行動版與桌機版大小差距過大,調整平板這類中間螢幕尺寸的 RWD 設定,變得異常耗時。
1. 鍵盤也要能用
只有滑鼠才觸發的效果,鍵盤使用者看不到。例如縮圖「滑鼠移上去(mouseover)就顯示大圖、移開(mouseout)就隱藏」,鍵盤使用者沒有滑鼠可以移,就永遠看不到大圖。需要「搭配對應的鍵盤事件」。滑鼠移入/移出,對應的是聚焦(focus)和失焦(blur),例如:
thumb.addEventListener('mouseover', showImg)
thumb.addEventListener('mouseout', hideImg)
// 鍵盤版:Tab 聚焦時顯示、離開時隱藏
thumb.addEventListener('focus', showImg)
thumb.addEventListener('blur', hideImg)
💡 前提是這個元素本身要能被聚焦,像是
<a>、<button>,或加了tabindex="0",否則focus事件也不會發生。
同理,用 <div> 做的「按鈕」,click 事件對於 <div tabindex="0"> 不會被 Enter 觸發,要自己補程式碼。或者改用 <button>。
2. 自製元件加上 ARIA
如果遇到需要自製 Tabs、Dialog、Combobox 之類,至少需要可聚焦(tabindex="0")、有 role、有鍵盤操作(Enter/Space、方向鍵、Esc)。元件的鍵盤互動規範,可以直接查 W3C 的 ARIA Authoring Practices Guide(APG)。
3. 焦點管理
<dialog> 搭配 showModal(),因為瀏覽器已經幫你處理了大部分的事,真香!const dialog = document.querySelector('dialog')
openBtn.addEventListener('click', () => dialog.showModal())
4. 動態更新要被「說出來」
畫面上出現錯誤訊息、Toast,對看得到畫面的人是一目了然,但螢幕閱讀器不會自己知道。要用 live region 通知它:
<!-- 一般通知 -->
<div role="status" aria-live="polite">{{ message }}</div>
<!-- 緊急錯誤 -->
<div role="alert">{{ errorMessage }}</div>
要注意,live region 的容器要在頁面載入時就存在,再去更新裡面的內容,才會被朗讀。載入中的動畫也一樣,除了動畫之外,給一個文字狀態(或 aria-busy)。至於表單驗證,MDN 的建議是送出時才驗證,不要每打一個字就跳錯誤;錯誤摘要放在前面,並讓每一條都連到對應的欄位。
其他還有:
拖曳要有替代操作:前面做的拖曳和排序,都可以提供鍵盤或點選的方式完成同一件事。例如排序的話加「上移/下移」按鈕,配對的話改成「選取項目後再選目標」。
漸進增強:MDN 建議先用純 HTML 把基本功能做出來,再用 JavaScript 往上加強,而不是整個頁面都靠 JS 才長得出來。在 SPA 專案中可以理解成,能用原生元素就不要自己用 JS 重做,像是表單用 <form> 和 <button type="submit">、連結用 <a href>,這樣即使 JS 出了狀況,基本行為還是由瀏覽器負責。
本來覺得無障礙網頁做起來很麻煩,透過這次寫文章才發現,依循瀏覽器原生行為、HTML 標籤正確使用,就算是打好基本盤。再來自製元件加上 ARIA、動畫支援 prefers-reduced-motion 等設定,也算是完成大部分了。剩下就是檢查是否能單靠鍵盤操作主要流程。如此一來,就能把「好看」補成「好用」,讓專案更上一層樓。
.
.
.
Day29 Clear!倒數剩下 1 天! キタ━━━(゜∀゜)━━━!!!!!