📚 本課學習目標
- 說明無障礙(accessibility)是什麼、為什麼它是每個網頁的基本要求,以及 WCAG 的四大原則與 A / AA / AAA 等級
- 先用正確的 HTML 語義解決大多數問題,只在必要時才加 ARIA
- 寫出鍵盤可以完整操作的頁面:焦點順序、可見的焦點樣式、跳過連結、不可有鍵盤陷阱
- 替圖片、表單、顏色、動畫、影音做出符合 WCAG 2.2 AA 的決定
- 用 Lighthouse、axe、鍵盤與螢幕閱讀器(VoiceOver / NVDA)檢查自己的頁面
本課把前面 30 課學過的 HTML、CSS、JavaScript 重新檢視一次:同樣的功能,怎麼寫才能讓用鍵盤、螢幕閱讀器、放大鏡、語音控制的使用者都能使用。
🌟 什麼是無障礙?為什麼重要?
無障礙指的是身心障礙者也能感知、操作、理解網頁。受益的人比想像中多:
| 情況 | 需要什麼 |
|---|---|
| 全盲、低視能 | 螢幕閱讀器朗讀、足夠的對比、可放大到 200% 不跑版 |
| 色覺異常(男性約 8%) | 不能只靠顏色傳達資訊 |
| 肢體障礙、暫時受傷(手打石膏) | 完全用鍵盤或語音操作 |
| 聽障 | 影片字幕、音訊逐字稿 |
| 認知與學習障礙、注意力不足 | 清楚的結構、一致的操作、可關閉的動畫 |
| 所有人在陽光下、單手抱小孩、網路很慢時 | 以上全部 |
此外,無障礙在許多地區是法規要求(台灣的「網站無障礙規範 2.0」、歐盟 EN 301 549、美國 ADA / Section 508),而且和 SEO 高度重疊:搜尋引擎「看」網頁的方式和螢幕閱讀器很像。
WCAG:衡量的標準
WCAG(Web Content Accessibility Guidelines)由 W3C 制定,目前版本 2.2(2023)。四大原則縮寫 POUR:
| 原則 | 意思 | 例子 |
|---|---|---|
| Perceivable 可感知 | 資訊不能只有一種感官能接收 | 圖片要有替代文字、影片要有字幕、對比要夠 |
| Operable 可操作 | 所有功能都能用鍵盤完成、不設時間陷阱 | Tab 可以到每個按鈕、沒有閃爍超過 3 次/秒的內容 |
| Understandable 可理解 | 內容與操作方式可預期 | 頁面語言正確、錯誤訊息說明怎麼修正 |
| Robust 穩健 | 各種瀏覽器與輔助科技都能解讀 | 合法的 HTML、正確的 name / role / value |
每條成功準則(success criterion)有等級 A(最低)、AA(法規與業界的一般目標)、AAA(加強)。本課以 AA 為目標。
🏗️ 原則一:先用語義化 HTML,ARIA 是最後手段
第 8 課學過的語義化標籤,就是無障礙的基礎。瀏覽器會把 HTML 轉成無障礙樹(accessibility tree),每個節點有 role(角色)、name(名稱)、state(狀態),螢幕閱讀器讀的是這棵樹。
<!-- ❌ 螢幕閱讀器只聽到「可點擊」甚至什麼都沒有,鍵盤 Tab 也到不了 -->
<div class="btn" onclick="save()">儲存</div>
<!-- ✅ 自帶 role=button、可聚焦、Enter / Space 都能觸發、名稱是「儲存」 -->
<button type="button" onclick="save()">儲存</button>
ARIA 的第一條規則:能用原生 HTML 就不要用 ARIA。 原生元素免費附帶鍵盤行為與狀態;ARIA 只會改變「說法」,不會改變「行為」。
| 需求 | 用這個 | 不要用 |
|---|---|---|
| 按鈕 | <button> |
<div role="button"> |
| 連結 | <a href> |
<span onclick> |
| 勾選 | <input type="checkbox"> |
<div role="checkbox"> |
| 區塊標題 | <h1>–<h6> |
<div class="title"> |
| 導覽、主要內容 | <nav>、<main> |
<div role="navigation"> |
文件層級的三件事
<!DOCTYPE html>
<html lang="zh-Hant-TW"> <!-- 1. 語言:決定螢幕閱讀器用哪種語音 -->
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <!-- 不要加 user-scalable=no -->
<title>訂單查詢 - 好棒商店</title> <!-- 2. 每頁唯一、先寫頁面主題 -->
</head>
<body>
<a class="skip-link" href="#main">跳到主要內容</a> <!-- 3. 跳過連結 -->
<header>…</header>
<nav aria-label="主選單">…</nav>
<main id="main">
<h1>訂單查詢</h1> <!-- 每頁一個 h1,標題不跳級 -->
…
</main>
<footer>…</footer>
</body>
</html>
標題(heading)是螢幕閱讀器使用者最常用的導覽方式(按 H 鍵在標題間跳)。標題層級要反映結構,不是字體大小:想要小一點的 h2,用 CSS 改,不要改成 h4。
🖼️ 原則二:圖片與替代文字
<!-- 資訊性圖片:alt 寫出「這張圖在這裡要傳達什麼」 -->
<img src="chart-q3.png" alt="2024 年第三季營收長條圖:七月 120 萬、八月 150 萬、九月 180 萬,逐月成長">
<!-- 功能性圖片(圖片就是按鈕/連結):alt 寫動作,不寫外觀 -->
<a href="/"><img src="logo.svg" alt="好棒商店首頁"></a>
<button type="button"><img src="search.svg" alt="搜尋"></button>
<!-- 裝飾性圖片:alt="" 讓螢幕閱讀器完全略過(不要省略 alt 屬性!) -->
<img src="divider.png" alt="">
<!-- 複雜圖片:短 alt + 旁邊的長描述 -->
<figure>
<img src="floorplan.png" alt="三樓平面圖,詳細說明見下方">
<figcaption>三樓:電梯出口左轉為會議室 A、B,右轉為茶水間與洗手間。</figcaption>
</figure>
判斷流程:這張圖拿掉後,資訊有沒有少? 沒有 → alt="";有 → 把少掉的資訊寫進 alt。不要以「圖片」「照片」開頭(螢幕閱讀器已經會說「圖片」),不要把檔名當 alt。CSS background-image 不會被朗讀,只能放裝飾圖。
⌨️ 原則三:鍵盤操作與焦點
很多使用者完全不用滑鼠。檢查方式很簡單:把滑鼠移開,只用 Tab / Shift+Tab / Enter / Space / 方向鍵操作整個頁面。
焦點順序與 tabindex
- 原生可互動元素(
a[href]、button、input、select、textarea)天生可聚焦,Tab 順序 = DOM 順序。CSS 改變視覺位置(order、flex-direction: row-reverse、絕對定位)不會改變 Tab 順序,所以 DOM 順序就要是合理的閱讀順序。 tabindex="0":讓自訂元件加入 Tab 順序(搭配 role 與鍵盤事件)。tabindex="-1":可以用 JavaScriptfocus()聚焦,但 Tab 不會停。用於對話框容器、錯誤摘要、跳過連結的目的地。tabindex="1"以上:不要用。 它會把元素插到所有東西前面,順序馬上亂掉。
焦點一定要看得見(WCAG 2.4.7,A 級)
/* ❌ 最常見的無障礙破壞:為了「好看」拿掉焦點框 */
*:focus { outline: none; }
/* ✅ 只在鍵盤操作時顯示,滑鼠點擊不顯示;對比至少 3:1 */
:focus-visible {
outline: 3px solid #1a56db;
outline-offset: 2px;
}
WCAG 2.2 新增 2.4.11 Focus Not Obscured(AA):取得焦點的元素不能被固定的頁首、cookie 橫幅等完全遮住。固定頁首下方的內容記得加 scroll-margin-top。
跳過連結(skip link)
每頁開頭的第一個可聚焦元素,讓鍵盤使用者不用 Tab 過 40 個選單項目:
.skip-link {
position: absolute;
left: 0; top: -100px; /* 平常藏在畫面外 */
background: #1a56db; color: #fff; padding: 8px 16px;
}
.skip-link:focus { top: 0; } /* 取得焦點時出現 */
目的地 <main id="main" tabindex="-1">,否則部分瀏覽器只捲動、不移動焦點。
鍵盤陷阱與對話框
焦點進得去就要出得來(WCAG 2.1.2)。自製對話框(modal)要做到:開啟時把焦點移進去、Tab 在對話框內循環、Esc 關閉、關閉後焦點回到原本的按鈕。最簡單的做法是用原生 <dialog>:
<button type="button" id="open">刪除帳號</button>
<dialog id="confirm" aria-labelledby="confirm-title">
<h2 id="confirm-title">確定要刪除帳號?</h2>
<p>此動作無法復原。</p>
<form method="dialog">
<button value="cancel">取消</button>
<button value="ok">確定刪除</button>
</form>
</dialog>
<script>
const dialog = document.getElementById('confirm');
document.getElementById('open').addEventListener('click', () => dialog.showModal());
// showModal() 會自動:鎖住背景、把焦點移入、支援 Esc、關閉後把焦點還給開啟的按鈕
</script>
自訂元件的鍵盤慣例
如果真的要自己做(例如折疊面板、分頁籤),遵守 WAI-ARIA Authoring Practices 的鍵盤慣例:按鈕用 Enter 與 Space、分頁籤用左右方向鍵、選單用上下方向鍵、Esc 關閉。
<!-- 折疊區塊(disclosure):原生按鈕 + 一個 ARIA 狀態 -->
<button type="button" aria-expanded="false" aria-controls="faq-1">運費怎麼算?</button>
<div id="faq-1" hidden>滿 1,000 元免運,未滿收 80 元。</div>
<script>
document.querySelectorAll('[aria-expanded]').forEach(btn => {
btn.addEventListener('click', () => {
const open = btn.getAttribute('aria-expanded') === 'true';
btn.setAttribute('aria-expanded', String(!open));
document.getElementById(btn.getAttribute('aria-controls')).hidden = open;
});
});
</script>
螢幕閱讀器會念「運費怎麼算?按鈕,已收合」,按下後變成「已展開」。更簡單的情況直接用原生 <details><summary>,連 JavaScript 都不用。
📝 原則四:表單
第 6、7、26 課的表單,加上這幾件事就符合 AA:
<form novalidate>
<!-- 1. 每個欄位都有程式上關聯的 label(點 label 會聚焦欄位,就代表關聯成功) -->
<label for="email">電子郵件(必填)</label>
<input id="email" name="email" type="email" required autocomplete="email"
aria-describedby="email-hint email-error" aria-invalid="false">
<p id="email-hint">我們只用來寄送訂單通知。</p>
<p id="email-error" class="error" hidden></p>
<!-- 2. 一組選項用 fieldset + legend,螢幕閱讀器才知道「這些單選鈕是在問什麼」 -->
<fieldset>
<legend>取貨方式</legend>
<label><input type="radio" name="pickup" value="store"> 門市取貨</label>
<label><input type="radio" name="pickup" value="home"> 宅配到府</label>
</fieldset>
<button type="submit">送出訂單</button>
</form>
- 不要用
placeholder代替 label:輸入後就消失,對比通常也不夠。 - 錯誤要用文字說明怎麼修(WCAG 3.3.1、3.3.3),不能只把框線變紅(1.4.1「不能只靠顏色」)。送出失敗時把焦點移到第一個錯誤欄位或錯誤摘要,並用
aria-invalid="true"與aria-describedby讓錯誤訊息被一起朗讀。 autocomplete屬性(WCAG 1.3.5,AA)讓瀏覽器與輔助工具自動填入常見欄位:name、email、tel、postal-code、cc-number……- WCAG 2.2 新增 3.3.8 Accessible Authentication(AA):登入不能要求使用者「記住或抄寫」(例如禁止貼上密碼、要求辨識圖片拼圖),要允許密碼管理器與貼上。
錯誤處理範例(搭配第 26 課)
function showError(input, message) {
const error = document.getElementById(input.getAttribute('aria-describedby').split(' ').pop());
error.textContent = message;
error.hidden = false;
input.setAttribute('aria-invalid', 'true');
}
form.addEventListener('submit', (e) => {
const email = form.elements.email;
if (!email.validity.valid) {
e.preventDefault();
showError(email, '請輸入有效的電子郵件,例如 name@example.com');
email.focus(); // 把使用者帶到問題所在
}
});
即時訊息:aria-live
JavaScript 動態插入的訊息(「已加入購物車」「搜尋到 12 筆結果」)螢幕閱讀器不會自動唸。放進一個事先就存在的 live region:
<div id="status" role="status" aria-live="polite"></div> <!-- 一般通知 -->
<div id="alert" role="alert"></div> <!-- 緊急錯誤,會打斷朗讀 -->
<script>
document.getElementById('status').textContent = '已將「無線滑鼠」加入購物車';
</script>
🎨 原則五:顏色、對比、文字
| 準則 | 要求(AA) |
|---|---|
| 1.4.3 文字對比 | 一般文字 4.5:1;大字(≥ 24px,或 ≥ 18.66px 粗體)3:1 |
| 1.4.11 非文字對比 | 圖示、輸入框邊框、焦點框等 UI 元件與相鄰顏色 3:1 |
| 1.4.1 不只靠顏色 | 錯誤、必填、連結、圖表系列都要有第二種提示:文字、圖示、底線、花紋 |
| 1.4.4 文字縮放 | 瀏覽器放大到 200% 內容仍可用;用 rem/em,不要用 px 鎖死字級 |
| 1.4.10 重排 | 320px 寬(或放大 400%)時不出現水平捲軸——第 16 課的響應式設計 |
| 1.4.12 文字間距 | 使用者自行加大行高 1.5、段距 2、字距 0.12em 時內容不被裁切 |
/* 對比範例(白底) */
color: #767676; /* 4.54:1 — 剛好通過一般文字 AA */
color: #949494; /* 3.03:1 — 只勉強通過大字與 UI 元件的 3:1,一般文字不行 */
color: #999999; /* 2.85:1 — 什麼都不能用(設計稿裡最常見的「淺灰說明文字」) */
color: #1a56db; /* 6.2:1 — 連結、焦點框都安全 */
對比可以用 Chrome DevTools 的色彩選擇器、Firefox 的無障礙檢查器或 WebAIM Contrast Checker 量。深色模式要各自檢查一次。
🎬 原則六:動畫、動態與影音
- 尊重使用者設定(WCAG 2.3.3):
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
- 自動播放超過 5 秒的輪播、影片、跑馬燈要能暫停、停止或隱藏(2.2.2)。
- 任何東西每秒閃爍不得超過 3 次(2.3.1,A 級)——這條會誘發癲癇,沒有例外。
<video>要有字幕(<track kind="captions">,1.2.2 A 級);只有聲音的內容要有逐字稿;重要的純視覺內容要有口述影像或文字描述(1.2.5 AA)。- 不要用
autoplay播放有聲音的內容;若一定要,提供立即可見的靜音/暫停控制(1.4.2)。
🛠️ 原則七:怎麼測試
沒有任何單一工具能保證無障礙。三層檢查缺一不可:
- 自動化工具(抓得到約 30–40% 的問題)
- Chrome DevTools → Lighthouse → Accessibility
- axe DevTools 瀏覽器擴充功能
- Firefox 開發者工具 → 無障礙環境(可直接看無障礙樹與對比)
- HTML 驗證器(validator.w3.org):重複的
id、沒關的標籤都會影響輔助科技 - 手動檢查(10 分鐘) - 只用鍵盤走完主要流程:每個可互動元素都到得了?看得到焦點在哪?Esc 能關閉?有沒有卡住? - 把瀏覽器放大到 200%、視窗縮到 320px 寬 - 關掉 CSS(或用 Reader 模式)看內容順序是否合理 - 開啟作業系統的「減少動態效果」設定
- 用輔助科技實際操作 - macOS / iOS:VoiceOver(⌘ + F5);Windows:NVDA(免費);Android:TalkBack - 聽一遍:標題是否有意義、圖片替代文字是否合理、表單欄位是否都有名稱、動態訊息有沒有被唸出來
把「鍵盤可操作」「Lighthouse 無障礙 ≥ 95 分」加進第 30 課專題的驗收清單。
💻 實作練習:把一個表單改成無障礙版本
完整範例:examples/lesson-31/example-01-accessible-form.html
範例包含:跳過連結、正確的標題層級與地標、fieldset/legend、aria-describedby 的提示與錯誤訊息、aria-invalid、role="status" 的送出結果、:focus-visible 焦點樣式、prefers-reduced-motion、以及一個用 <button aria-expanded> 做的折疊 FAQ。打開後請:
- 全程只用鍵盤填完表單並送出。
- 故意留空必填欄位送出,觀察焦點跳到哪裡、螢幕閱讀器唸什麼。
- 用 Lighthouse 跑一次,再故意把
label的for拿掉、把outline: none加回去,看分數與警告怎麼變。
📝 無障礙檢查清單(AA)
- [ ]
<html lang>正確、每頁<title>唯一且有意義 - [ ] 每頁一個
h1,標題層級不跳級;main、nav、header、footer地標齊全 - [ ] 所有圖片有
alt(裝飾圖alt="") - [ ] 所有功能鍵盤可操作,焦點可見,順序合理,沒有陷阱,有跳過連結
- [ ] 每個表單欄位有
label;錯誤用文字說明;必填不只靠紅色星號 - [ ] 文字對比 ≥ 4.5:1、UI 元件 ≥ 3:1;資訊不只靠顏色
- [ ] 放大 200%、320px 寬皆可用;沒有
user-scalable=no - [ ] 動畫尊重
prefers-reduced-motion;自動播放可暫停;沒有每秒閃爍 > 3 次 - [ ] 影片有字幕;音訊有逐字稿
- [ ] 動態訊息放在
aria-live區域;自訂元件有正確的 role / name / state - [ ] Lighthouse 無障礙分數 ≥ 95,且實際用鍵盤與螢幕閱讀器走過主要流程
📝 本課重點回顧
- 無障礙的衡量標準是 WCAG 2.2,四原則 POUR,目標等級 AA。
- 語義化 HTML 先行;ARIA 只補原生做不到的狀態與名稱,而且不會替你加鍵盤行為。
- 鍵盤是最重要的測試工具:Tab 到得了、看得到焦點、出得來。
- 圖片的 alt 取決於「拿掉後少了什麼資訊」;表單的 label、錯誤訊息、autocomplete 缺一不可。
- 對比 4.5:1 / 3:1、不只靠顏色、尊重減少動態效果、影片要字幕。
- 自動化工具只能抓三到四成問題,一定要加上手動與輔助科技測試。
🎯 課後練習
- 下面這段程式碼有幾個無障礙問題?逐一指出並改寫。
<div class="card" onclick="location.href='/sale'"> <img src="sale.jpg"> <span style="color:red">限時特賣</span> </div> - 一個「加入購物車」按鈕按下後,頁面右上角出現 2 秒的「已加入」提示後消失。螢幕閱讀器使用者會錯過它。要怎麼修?
- 設計師給的次要文字顏色是
#999999(白底)。對比約 2.85:1。哪些地方可以用、哪些不行?你會怎麼跟設計師溝通? - 自製一個分頁籤(tabs)元件時,需要哪些 role、哪些 ARIA 屬性、哪些鍵盤操作?(提示:查 WAI-ARIA Authoring Practices 的 Tabs pattern。)
- 為什麼「
tabindex="1"、tabindex="2"把重要按鈕排在前面」是壞主意?正確的做法是什麼?
參考答案
- 至少五個問題:(a)
div不可聚焦、不是按鈕也不是連結——改成<a href="/sale">;(b) 圖片沒有alt——若是資訊圖寫alt="限時特賣:全館 8 折",若純裝飾寫alt="";(c) 「限時特賣」只靠紅色強調——加上文字或圖示,並確認紅色與白底對比 ≥ 4.5:1;(d)onclick沒有鍵盤對應——改用原生<a>後自然解決;(e) 行內樣式與整張卡片可點擊但沒有可聚焦的目標,螢幕閱讀器無法得知它可以操作。改寫:<a class="card" href="/sale"> <img src="sale.jpg" alt=""> <span class="badge">🔥 限時特賣</span> </a> - 把提示放進頁面載入時就存在的
<div role="status" aria-live="polite">,用 JavaScript 更新其textContent;視覺上可以 2 秒後淡出,但 DOM 文字可保留久一點(或至少讓 live region 有時間朗讀)。若是錯誤改用role="alert"。 - 2.85:1 連大字的 3:1 都不到,所以正文、說明文字、表單提示都不行;只能用在純裝飾(例如分隔線)或停用狀態的元件(1.4.3 的例外)。溝通方式:拿 WebAIM Contrast Checker 的數字給設計師看,並提出最接近的合格色(白底上
#767676= 4.54:1),說明這是 AA 法規要求而非個人喜好。 - 容器
role="tablist";每個籤role="tab"、aria-selected="true|false"、aria-controls="面板 id",只有選中的籤tabindex="0"、其餘-1;面板role="tabpanel"、aria-labelledby="籤 id"。鍵盤:Tab 進入選中的籤,左右方向鍵在籤之間移動(並自動或按 Enter/Space 啟用),Home/End 跳到第一/最後一個,Tab 再按一次進入面板內容。 - 正數
tabindex會讓這些元素排在所有沒有設定的元素之前(包含跳過連結與導覽),而且日後任何人新增元素都得維護這串數字,順序很快就和視覺不一致。正確做法是調整 DOM 順序讓重要元素本來就在前面,必要時用 CSS 排版;自訂元件只用tabindex="0"與-1。
🔗 延伸閱讀
- WCAG 2.2 快速參考(How to Meet WCAG)
- web.dev — Learn Accessibility(本課的章節順序參考此課程)
- MDN — Accessibility
- WAI-ARIA Authoring Practices Guide(元件模式與鍵盤慣例)
- WebAIM Contrast Checker
- 國家通訊傳播委員會《網站無障礙規範 2.0》