Web 無障礙實戰:ARIA 角色與 WCAG 2.2 合規指南

前端工程(更新於 2026年6月17日)

WCAG 2.2 四大原則

WCAG 2.2 圍繞 POUR 四大原則組織合規要求:

原則 英文 核心要求 範例
可感知 Perceivable 資訊可被所有使用者感知 圖片 alt、字幕、對比度
可操作 Operable 介面可透過多種方式操作 鍵盤導航、足夠時間、無閃爍
可理解 Understandable 內容與操作可被理解 一致導航、錯誤提示、輸入幫助
健壯 Robust 可被各類輔助技術解析 有效 HTML、ARIA 語義

ARIA 角色體系

文件結構角色

<header role="banner">
  <nav role="navigation" aria-label="主導航">
    <ul role="menubar">
      <li role="menuitem"><a href="/home">首頁</a></li>
      <li role="menuitem" aria-haspopup="true">
        產品
        <ul role="menu">
          <li role="menuitem"><a href="/saas">SaaS</a></li>
          <li role="menuitem"><a href="/api">API</a></li>
        </ul>
      </li>
    </ul>
  </nav>
</header>
<main role="main">
  <article role="article">
    <h2>文章標題</h2>
    <p>正文內容</p>
  </article>
</main>
<footer role="contentinfo">版權資訊</footer>

視窗小工具角色(Widget Roles)

角色 用途 必需屬性 關鍵互動
button 可點選觸發 Enter/Space 啟用
checkbox 選取/未選 aria-checked Space 切換
radio 單選項 aria-checked 方向鍵切換
slider 範圍值 aria-valuenow, aria-valuemin, aria-valuemax 方向鍵增減
tablist / tab 標籤頁 aria-selected 方向鍵切換
dialog 模態/非模態 aria-modal Esc 關閉
combobox 可選輸入 aria-expanded 上下選擇/輸入篩選
tree / treeitem 樹形結構 aria-expanded 方向鍵展開折疊

ARIA 屬性與狀態

核心屬性速查

<!-- 標籤與描述 -->
<button aria-label="關閉對話框">✕</button>
<input aria-labelledby="name-label" aria-describedby="name-hint">
<span id="name-label">使用者名稱</span>
<span id="name-hint">6-20個字元</span>

<!-- 狀態 -->
<div aria-expanded="true" aria-selected="false" aria-checked="mixed">
<div aria-hidden="true" aria-disabled="true" aria-busy="true">

<!-- 關係 -->
<div aria-owns="child-id" aria-controls="panel-id" aria-flowto="next-section">

<!-- 即時區域 -->
<div role="alert" aria-live="assertive">錯誤資訊</div>
<div aria-live="polite" aria-atomic="true" aria-relevant="additions text">狀態更新</div>

第一規則:不要濫用 ARIA

如果原生 HTML 元素已具備所需語義和互動,就不要用 ARIA 重新實作。

<!-- ❌ 反模式 -->
<div role="button" tabindex="0" onclick="submit()">提交</div>

<!-- ✅ 正確做法 -->
<button type="submit">提交</button>

實戰:無障礙模態框

<div class="modal-overlay" id="modalOverlay" hidden>
  <div
    role="dialog"
    aria-modal="true"
    aria-labelledby="modalTitle"
    aria-describedby="modalDesc"
    class="modal"
  >
    <h2 id="modalTitle">確認刪除</h2>
    <p id="modalDesc">此操作不可撤銷,確定要刪除該記錄嗎?</p>
    <div class="modal-actions">
      <button class="btn-cancel">取消</button>
      <button class="btn-confirm">確認刪除</button>
    </div>
  </div>
</div>
class AccessibleModal {
  constructor(dialogEl) {
    this.dialog = dialogEl;
    this.previousFocus = null;
  }

  open() {
    this.previousFocus = document.activeElement;
    this.dialog.hidden = false;
    const firstFocusable = this.dialog.querySelector(
      'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
    );
    firstFocusable?.focus();
    document.addEventListener('keydown', this.handleKeydown);
  }

  close() {
    this.dialog.hidden = true;
    this.previousFocus?.focus();
    document.removeEventListener('keydown', this.handleKeydown);
  }

  handleKeydown = (e) => {
    if (e.key === 'Escape') this.close();
    if (e.key === 'Tab') this.trapFocus(e);
  };

  trapFocus(e) {
    const focusable = [...this.dialog.querySelectorAll(
      'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
    )];
    const first = focusable[0];
    const last = focusable[focusable.length - 1];
    if (e.shiftKey && document.activeElement === first) {
      e.preventDefault();
      last.focus();
    } else if (!e.shiftKey && document.activeElement === last) {
      e.preventDefault();
      first.focus();
    }
  }
}

實戰:無障礙標籤頁

<div class="tabs">
  <div role="tablist" aria-label="帳戶設定">
    <button
      role="tab"
      id="tab-profile"
      aria-selected="true"
      aria-controls="panel-profile"
      tabindex="0"
    >個人資料</button>
    <button
      role="tab"
      id="tab-security"
      aria-selected="false"
      aria-controls="panel-security"
      tabindex="-1"
    >安全設定</button>
    <button
      role="tab"
      id="tab-notify"
      aria-selected="false"
      aria-controls="panel-notify"
      tabindex="-1"
    >通知偏好</button>
  </div>

  <div role="tabpanel" id="panel-profile" aria-labelledby="tab-profile">
    <p>個人資料設定內容</p>
  </div>
  <div role="tabpanel" id="panel-security" aria-labelledby="tab-security" hidden>
    <p>安全設定內容</p>
  </div>
  <div role="tabpanel" id="panel-notify" aria-labelledby="tab-notify" hidden>
    <p>通知偏好內容</p>
  </div>
</div>
document.querySelector('[role="tablist"]').addEventListener('keydown', (e) => {
  const tabs = [...e.currentTarget.querySelectorAll('[role="tab"]')];
  const idx = tabs.indexOf(document.activeElement);
  let nextIdx;

  if (e.key === 'ArrowRight') nextIdx = (idx + 1) % tabs.length;
  else if (e.key === 'ArrowLeft') nextIdx = (idx - 1 + tabs.length) % tabs.length;
  else if (e.key === 'Home') nextIdx = 0;
  else if (e.key === 'End') nextIdx = tabs.length - 1;
  else return;

  e.preventDefault();
  tabs.forEach((t) => { t.setAttribute('aria-selected', 'false'); t.tabIndex = -1; });
  tabs[nextIdx].setAttribute('aria-selected', 'true');
  tabs[nextIdx].tabIndex = 0;
  tabs[nextIdx].focus();
});

實戰:無障礙表單

<form aria-label="使用者註冊">
  <div class="field">
    <label for="email">電子郵件地址 <span aria-hidden="true">*</span></label>
    <input
      id="email"
      type="email"
      required
      aria-required="true"
      autocomplete="email"
      aria-invalid="false"
      aria-describedby="email-error"
    >
    <span id="email-error" role="alert" aria-live="assertive"></span>
  </div>

  <div class="field">
    <label for="password">密碼</label>
    <input
      id="password"
      type="password"
      minlength="8"
      aria-describedby="pwd-rules pwd-strength"
    >
    <span id="pwd-rules">至少8個字元,包含大小寫字母和數字</span>
    <span id="pwd-strength" aria-live="polite"></span>
  </div>

  <fieldset>
    <legend>通知方式</legend>
    <label><input type="checkbox" name="notify" value="email"> 郵件</label>
    <label><input type="checkbox" name="notify" value="sms"> 簡訊</label>
  </fieldset>

  <button type="submit">註冊</button>
</form>

自動化偵測工具對比

工具 型別 覆蓋 WCAG 整合方式 特點
axe-core 執行時函式庫 A/AA CI + 瀏覽器 最全面,零誤報
Lighthouse 稽核工具 A CLI / DevTools 綜合效能+無障礙
WAVE 瀏覽器擴充功能 A/AA 手動 視覺化標註
NVDA 螢幕閱讀器 手動 真實使用者體驗
Playwright a11y E2E 測試 A CI 與測試流程整合
import { test } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';

test('無障礙檢查', async ({ page }) => {
  await page.goto('/register');
  const results = await new AxeBuilder({ page })
    .withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
    .analyze();
  expect(results.violations).toEqual([]);
});

WCAG 2.2 新增要點

新準則 等級 說明
2.4.11 Focus Appearance AAA 焦點指示器面積 ≥ 2px,對比度 ≥ 3:1
2.4.12 Focus Not Obscured AAA 焦點元素不被其他內容遮擋
3.2.6 Consistent Help A 幫助機制位置在頁面間保持一致
3.3.7 Redundant Entry A 同一流程中不要求重複輸入已提供的資訊
3.3.8 Accessible Authentication AA 認證不依賴認知功能測試

最佳實踐總結

  1. 原生元素優先:用 <button><input> 等原生元素替代 ARIA 重造
  2. 焦點管理:模態/路由切換後主動聚焦到合理位置
  3. 即時區域審慎aria-live="assertive" 僅用於關鍵通知,避免頻繁打斷
  4. 自動化 + 手動:axe-core 覆蓋約 30-40% 問題,螢幕閱讀器手動測試不可省略
  5. 持續整合:將無障礙偵測納入 CI 流水線,防止迴歸

本站提供瀏覽器本地工具,免註冊即可試用 →

#无障碍#ARIA#WCAG#屏幕阅读器#可访问性