核心原則

鍵盤使用者導航三要素:focus 可見、tab 順序合理、有 escape 路徑。 三者任一缺失、鍵盤使用者就卡住。視覺使用者看不到 focus 也能用滑鼠繼續、鍵盤使用者沒有 fallback。

本篇焦點:鍵盤可達性


為什麼鍵盤可達性需要獨立盤點

使用者類型

使用者為什麼用鍵盤
全盲(screen reader 使用者)完全靠鍵盤、滑鼠看不到游標位置
低視力鍵盤比滑鼠精準(不需要瞄準)
Motor 障礙鍵盤比滑鼠手部負擔小
Power user鍵盤比滑鼠快

最後一類占人口比例不小 — 鍵盤可達性對全體使用者都有價值、不只 a11y 使用者。

三要素的失敗模式

要素失敗模式後果
Focus 可見outline: 0 移除預設 focus 但沒補替代鍵盤使用者不知道 focus 在哪、迷失
Tab 順序順序跟視覺布局不一致跳來跳去、迷失
Escape 路徑Modal 沒有 ESC 關閉卡在 modal 出不來

三者都是「視覺使用者通常不會碰到、鍵盤使用者必碰」— 開發者用滑鼠測 100% OK、鍵盤使用者一進去就壞。


風險點 1:Focus indicator 的可見度

位置:tab focus 到 search input、scope radio、filter checkbox 等元素。

判讀

  • 瀏覽器預設 focus outline(藍色 2px)
  • 某些 theme 用 outline: 0 移除 — 鍵盤使用者迷失
  • 自訂 outline 要對比足夠(WCAG 2.4.7、AA 3:1 對比 + 至少 2px 寬)

症狀:鍵盤使用者 tab 過去看不到 focus 在哪、不知道下一個 enter 會激活誰。

第一個該查的:用 keyboard tab 過所有互動元素、確認每個都有可見 focus。

修正方向

 1/* 預設 — 信任瀏覽器 outline */
 2/* 不寫 outline: 0 */
 3
 4/* 客製 — 用 :focus-visible(只在鍵盤觸發時顯示、滑鼠點擊不顯示) */
 5:focus-visible {
 6  outline: 2px solid currentColor;
 7  outline-offset: 2px;
 8}
 9
10/* 移除 outline 必須補 box-shadow / border 等替代 */
11button:focus { outline: 0; box-shadow: 0 0 0 3px var(--focus-color); }

:focus-visible 是現代做法 — 滑鼠使用者不看到 outline(不會覺得「煩」)、鍵盤使用者看到 outline(必要的回饋)。

Focus indicator 的對比度

WCAG 2.4.11 要求 focus indicator 跟相鄰背景對比 ≥ 3:1:

1/* 較差 — 灰底 + 灰 outline、對比不足 */
2.button { background: #f0f0f0; }
3.button:focus-visible { outline: 2px solid #cccccc; }
4
5/* 好 — 跟背景對比足夠 */
6.button:focus-visible { outline: 2px solid #0066cc; }

風險點 2:Tab 順序與視覺布局的對齊

位置:搜尋頁元素:H1 → search input → scope radio → results → filter sidebar。

判讀

預設 tab 順序 = DOM 順序。如果視覺布局跟 DOM 順序不一致(例如 sidebar 在右、但 DOM 在前)、鍵盤使用者體驗:

  • Tab 1:H1(OK)
  • Tab 2:跑到 sidebar(視覺在右下、鍵盤跳過去)
  • Tab 3:search input(視覺在左上、鍵盤跳回來)

症狀:鍵盤使用者 tab 順序看似隨機、失去空間感。

第一個該查的:用 keyboard tab 過所有互動元素、看 focus 移動順序是否符合視覺閱讀順序(左到右、上到下)。

修正方向

策略機制
DOM 順序對齊視覺順序改 HTML 結構讓 DOM 順序就是 tab 順序
tabindex 調整順序顯式控制 tab 順序(風險:違反 DOM 順序、對 screen reader 仍依 DOM)
Skip link 跳過長 navigation讓鍵盤使用者快速跳到主內容

預設選「DOM 順序對齊視覺順序」 — 不需要 tabindex、對所有 a11y 工具都正確。

1<body>
2  <a href="#main" class="skip-link">跳到主內容</a>
3  <nav>...</nav>
4  <main id="main">...</main>
5</body>
 1.skip-link {
 2  position: absolute;
 3  top: -40px;       /* 預設藏起來 */
 4  left: 0;
 5  background: var(--bg);
 6  padding: 8px;
 7}
 8.skip-link:focus {
 9  top: 0;            /* tab 到時顯示 */
10}

第一個 tab 焦點 = skip link、鍵盤使用者可以選擇跳過 nav 直達主內容。


風險點 3:Modal / overlay 的 escape 路徑

位置:Pagefind drawer 在 mobile 模式展開、filter sidebar 在某些 layout 是 modal-like。

判讀

鍵盤使用者進入 modal 後需要:

  1. 按 ESC 可以關閉
  2. Tab 順序限制在 modal 內(focus trap、不會 tab 到背景元素)
  3. 關閉 modal 後 focus 回到觸發元素

任一缺失 = 卡住。

症狀:鍵盤使用者打開 filter drawer 後 tab 跑到背景元素、不知道怎麼關 drawer。

第一個該查的:開啟 modal / drawer / overlay、按 ESC 看會不會關、tab 看會不會跑到背景。

修正方向

 1function openModal(modal, trigger) {
 2  modal.showModal?.() || (modal.style.display = 'block');
 3
 4  // ESC 關閉
 5  modal.addEventListener('keydown', function (e) {
 6    if (e.key === 'Escape') closeModal(modal, trigger);
 7  });
 8
 9  // Focus trap(簡化版)
10  var focusables = modal.querySelectorAll('button, input, select, [tabindex]');
11  focusables[0]?.focus();
12
13  modal.addEventListener('keydown', function (e) {
14    if (e.key !== 'Tab') return;
15    var first = focusables[0];
16    var last = focusables[focusables.length - 1];
17    if (e.shiftKey && document.activeElement === first) {
18      e.preventDefault(); last.focus();
19    } else if (!e.shiftKey && document.activeElement === last) {
20      e.preventDefault(); first.focus();
21    }
22  });
23}
24
25function closeModal(modal, trigger) {
26  modal.close?.() || (modal.style.display = 'none');
27  trigger?.focus();  // 焦點回觸發元素
28}

<dialog> 元素自動 trap

1<dialog id="filter-modal">...</dialog>
1modal.showModal();  // 自動 focus trap + ESC 處理

<dialog> 是現代做法 — 鍵盤行為由瀏覽器處理、不需要手寫 trap 邏輯。


設計取捨:focus 處理策略

當需要客製 focus 視覺時、四種做法:

A:信任瀏覽器預設 outline(這個專案的預設)

  • 機制:完全不寫 outline 規則、瀏覽器藍色 outline 自動套用
  • 選 A 的理由:成本最低、跨瀏覽器一致、不會意外破壞
  • 適合:對 focus 視覺沒有強烈品牌需求
  • 代價:focus 看起來「不夠精緻」(瀏覽器預設不一定符合品牌風格)

B:用 :focus-visible 客製 outline

  • 機制:focus-visible { outline: 2px solid var(--brand); }、滑鼠點擊不顯示
  • 跟 A 的取捨:B 達到品牌一致性、滑鼠使用者不被「煩」;A 簡單但視覺一般
  • B 比 A 好的情境:品牌設計嚴格要求 focus 視覺

C:用 box-shadow 取代 outline

  • 機制:focus-visible { box-shadow: 0 0 0 3px var(--focus); outline: 0; }
  • 跟 B 的取捨:C 跟 outline 視覺差異是「跟著元素圓角」、適合圓角 UI;outline 永遠是矩形
  • C 比 B 好的情境:圓角元素需要 focus 跟隨圓角

D:完全移除 focus indicator

  • 機制*:focus { outline: 0; }、不補替代
  • 成本特別高的原因:違反 WCAG 2.4.7、鍵盤使用者完全無法導航
  • D 是反模式:違反 WCAG 2.4.7(合規層) — 即使品牌追求極簡、也該保留 focus indicator

「邏輯 tab 順序」要素的詳細展開(DOM vs tabindex 的取捨、跟 mental model 對齊)見 #71 Tab Order = DOM Order = Mental Model 三者對齊


跟其他原則的關係

關係
#37 Focus management on DOM move互補 — 本篇處理「靜態 focus 設計」、#37 處理「DOM 移動時 focus 該怎麼跟」
#39 Native HTML 優先於 ARIA role<button> / <dialog> / <input> 等 native element、自動獲得正確 keyboard 行為
#45 跟外部組件合作的層次客製 focus 樣式時、注意不要打破 framework 內部的 focus 邏輯

開發階段檢查清單

檢查動作
Focus 可見拔掉滑鼠、只用鍵盤、tab 過所有互動元素、確認每個都有可見 focus
Focus 對比DevTools Contrast Ratio 量 focus indicator 跟背景對比 ≥ 3:1
Tab 順序tab 過去確認順序符合視覺閱讀順序
ESC 關閉開啟 modal / drawer、按 ESC 看會不會關
Focus trap開啟 modal、tab 看是否限制在 modal 內
Focus return關閉 modal、看 focus 是否回觸發元素

每個 ~30 秒、開發完成前跑一輪。


判讀徵兆

訊號該檢查的位置
鍵盤使用者反映「不知道 focus 在哪」確認沒有 outline: 0 沒補替代、用 :focus-visible
Tab 順序看起來隨機DOM 順序對齊視覺順序、必要時用 skip link
Modal 開啟後鍵盤使用者卡住加 ESC 關閉 + focus trap、或改用 <dialog>
Modal 關閉後 focus 跑到頁面開頭關閉時手動 trigger.focus()
Focus 在 dark mode 看不清加對比度檢查(≥ 3:1)

核心原則:鍵盤可達性的三要素都是「視覺使用者通常不會碰、鍵盤使用者必碰」 — 開發階段必須拔滑鼠測一輪、不能依賴使用者通報。