36. tabindex

全域屬性 tabindex 控制元素是否可以取得焦點,以及如何參與循序鍵盤導覽。

使用者通常以 Tab 移至下一個可取得焦點的元素,並以 Shift + Tab 向後移動。

<div tabindex="0">
  這個元素可以取得鍵盤焦點。
</div>

不過,讓元素可以取得焦點,不會自動令它具有互動功能或符合無障礙需要。

如果原生控制元素符合預期行為,應優先使用 <button>、<a href> 和 <input> 等元素。

36.1 主要值

值意思
0將元素加入一般循序焦點次序
-1將元素排除於循序焦點導覽之外,但允許透過程式碼設定焦點
正整數將元素排在 tabindex="0" 及一般原生 Tab 停留點之前

雖然可以使用其他負整數,但負值模式應使用 -1。

大部分應用程式應只使用 0 和 -1。正值通常會令鍵盤導覽更難維護及理解。

實際鍵盤行為可能因瀏覽器、作業系統設定及元素而有所不同。

36.2 原生可取得焦點的元素

許多 HTML 元素已經支援鍵盤焦點:

<a href="course.html">查看課程</a>

<button type="button">開啟預覽</button>

<label for="email">電郵</label>
<input id="email" type="email">

這些元素通常不需要 tabindex="0"。

原生按鈕也提供:

  • 按鈕語意。
  • 鍵盤啟動行為。
  • 停用行為。
  • 與表單及輔助技術的整合。

相比之下:

<div tabindex="0">開啟預覽</div>

這只會建立可以取得焦點的元素,不會建立按鈕,也不會自動像按鈕一樣回應 Enter 或空白鍵。

36.3 tabindex="0"

使用 tabindex="0",將原本不能取得焦點的元素加入一般循序焦點次序:

<div tabindex="0" role="region" aria-label="可捲動範例">
  <!-- 內容 -->
</div>

適當用途之一,是鍵盤使用者需要操作的可捲動區域。

另一種用途,是自訂複合控制元件中的目前項目,例如採用移動式 tabindex 模式的分頁清單。

不要為每個標題、段落或容器加入 tabindex="0"。輔助技術通常可以透過閱讀指令存取靜態內容,不需要額外 Tab 停留點。

每個新增的 Tab 停留點都應具有清晰用途。

36.4 tabindex="-1"

如果元素需要透過程式碼取得焦點,但不應成為一般 Tab 停留點,可以使用 tabindex="-1":

<h2 id="results-heading" tabindex="-1">搜尋結果</h2>
document.getElementById("results-heading").focus();

常見用途包括:

  • 在使用者主動導覽後,將焦點移至標題。
  • 提交表單後,將焦點移至錯誤摘要。
  • 管理自訂控制元件內的焦點。
  • 為跳過連結模式提供可靠目標。

tabindex="-1" 不會隱藏或停用元素。

它也不保證指標操作無法令元素取得焦點。這種行為取決於元素及瀏覽器。

36.5 避免正值

正值會建立獨立的優先次序:

<button type="button" tabindex="2">第二優先</button>
<button type="button" tabindex="1">第一優先</button>
<button type="button">一般次序</button>

即使 tabindex="1" 的按鈕在原始碼中較後出現,它仍然會先於 tabindex="2" 的按鈕取得焦點。

正值按由小至大的次序處理。具有相同正值的元素,通常按文件次序排列。之後才輪到一般 Tab 停留點及 tabindex="0" 的元素。

內容變更時,這種做法很容易出問題。

應修正文件結構,而不是為可取得焦點的元素編號。

36.6 焦點次序與視覺次序

一般焦點次序通常遵循文件次序,而不是 CSS 建立的視覺位置。

例如,Flexbox 或 Grid 可以改變內容的視覺排列,但鍵盤導覽仍然使用不同次序:

.actions {
  display: flex;
}

.actions .last-in-source {
  order: -1;
}

不要透過正值 tabindex 補救。

應:

  • 按合乎邏輯的次序排列原始碼內容。
  • 讓視覺排列與該次序保持一致。
  • 使用 Tab 及 Shift + Tab 測試網頁。
  • 確保焦點次序保留內容意思及可操作性。

焦點次序不必對應每個視覺細節,但應可預測且容易理解。

36.7 可取得焦點不等於具有互動功能

以下不是完整的自訂按鈕:

<div tabindex="0" role="button">儲存</div>

role="button" 傳達角色,tabindex="0" 允許取得焦點,但兩者都不會實作按鈕行為。

作者仍然需要處理鍵盤啟動、指標輸入、停用狀態及其他細節。

應優先使用:

<button type="button">儲存</button>

同樣,為圖片加入 tabindex 不會令它變成連結,為容器加入它也不會令容器變成符合無障礙需要的應用程式。

建立自訂控制元素前,應先考慮原生 HTML。

36.8 可見焦點提示

鍵盤使用者需要看見目前哪個元素具有焦點。

使用 :focus-visible 提供清晰提示:

:focus-visible {
  outline: 3px solid #005ea8;
  outline-offset: 3px;
}

不要移除瀏覽器焦點外框,卻沒有提供有效替代:

/* 如果沒有替代焦點提示,這種寫法並不適當。 */
:focus {
  outline: none;
}

應檢查提示是否:

  • 具有足夠對比度。
  • 沒有被周圍容器裁切。
  • 在高對比度或強制色彩模式下保持可見。
  • 沒有被固定頁首或其他覆蓋內容遮擋。

可以取得焦點卻沒有可見焦點提示的元素,很難操作。

36.9 透過程式碼設定焦點

使用 .focus() 移動焦點:

const heading = document.getElementById("results-heading");

heading.focus();

標題通常需要 tabindex="-1" 才能支援這種模式。

如果不希望設定焦點時捲動網頁:

heading.focus({ preventScroll: true });

只有在目標仍然可見,或另外管理捲動時,才使用這個選項。

應因應有意義的使用者操作或應用程式變更移動焦點。不要在網頁載入時,或使用者輸入期間,不必要地搶走焦點。

替換內容時,應考慮目前具有焦點的元素是否會被移除。失去該元素可能令使用者無法確定目前位置。

36.10 跳過連結

跳過連結讓鍵盤使用者略過重複導覽:

<a href="#main-content" class="skip-link">跳至主要內容</a>

<header>
  <!-- 網站導覽 -->
</header>

<main id="main-content" tabindex="-1">
  <h1>課程資訊</h1>
</main>

負值讓主要區域可以取得焦點,但不會成為額外的一般 Tab 停留點。

設定樣式,讓連結在取得焦點時顯示:

.skip-link {
  position: absolute;
  inset-inline-start: 1rem;
  top: 1rem;
  transform: translateY(-200%);
}

.skip-link:focus {
  transform: translateY(0);
}

應測試啟動連結後,是否將使用者帶到預期內容,以及焦點是否被遮擋。

36.11 移動式 tabindex

部分複合控制元件會讓整個元件只佔用一個 Tab 停留點。

在**移動式 tabindex(roving tabindex)**模式中:

  • 一個項目具有 tabindex="0"。
  • 其他項目具有 tabindex="-1"。
  • 方向鍵在項目之間移動焦點。
  • JavaScript 更新哪個項目具有 tabindex="0"。

如果按照預期鍵盤互動方式實作,這種模式可以適用於分頁或工具列等控制元件。

單靠屬性不會實作這個模式。應用程式也必須管理:

  • 適當角色及狀態。
  • 鍵盤指令。
  • 焦點移動。
  • 選取或啟動行為。
  • 適用情況下的停用項目。

部分控制元件也可以使用 aria-activedescendant 作為另一種焦點管理方式。

沒有清晰理由時,不要將這些模式套用至一般網站導覽。標準連結通常使用原生行為最合適。

36.12 隱藏、停用與 Inert 內容

tabindex 不能取代可見性或停用狀態管理。

功能一般效果
tabindex="-1"將元素排除於一般循序焦點導覽之外
支援控制元素上的 disabled停用控制元素,通常也將它排除於焦點導覽之外
hidden 或 display: none通常阻止呈現及取得焦點
inert阻止子樹內的互動及焦點,並將它排除於無障礙資訊之外
aria-hidden="true"對無障礙 API 隱藏內容,但本身不會阻止取得焦點
aria-disabled="true"傳達停用狀態,但本身不會停用行為

不要在標示為 aria-hidden="true" 的內容內留下可取得焦點的控制元素。

模態介面應在適當情況下優先使用原生 <dialog>,並測試焦點位置、焦點限制、關閉及焦點還原。不要透過任意正值臨時拼湊焦點限制機制。

36.13 完整範例:跳過連結與可捲動程式碼

以下獨立網頁示範兩種具有明確用途的做法:

  • 在跳過連結目標上使用 tabindex="-1"。
  • 在可水平捲動的程式碼範例上使用 tabindex="0"。

原生連結及按鈕保留一般焦點行為。

id: hong-kong-html-tabindex
name: tabindex — 跳過連結與可捲動範例
type: html
content: |-
  <!doctype html>
  <html lang="zh-Hant-HK">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta
      name="description"
      content="示範如何為跳過連結及可捲動程式碼區域適當使用 tabindex。"
    >
    <title>tabindex | 鍵盤導覽範例</title>
    <style>
      *,
      *::before,
      *::after {
        box-sizing: border-box;
      }

      body {
        margin: 0;
        color: #222;
        background: #f5f7fa;
        font-family: system-ui, sans-serif;
        line-height: 1.8;
      }

      header,
      main {
        max-width: 44rem;
        margin-inline: auto;
        padding: 1.5rem 1rem;
      }

      header {
        padding-block-end: 0;
      }

      nav ul {
        display: flex;
        flex-wrap: wrap;
        gap: 0.5rem 1.5rem;
        margin: 0;
        padding: 0;
        list-style: none;
      }

      h1,
      h2 {
        color: #005ea8;
        line-height: 1.4;
      }

      section {
        min-width: 0;
        margin-block: 1.5rem;
        padding: 1.25rem;
        border: 1px solid #767676;
        border-radius: 0.5rem;
        background: white;
      }

      section > h2 {
        margin-block-start: 0;
      }

      a {
        color: #005ea8;
        text-underline-offset: 0.2em;
      }

      button {
        padding: 0.6rem 1rem;
        border: 1px solid #005ea8;
        border-radius: 0.3rem;
        color: white;
        background: #005ea8;
        font: inherit;
        cursor: pointer;
      }

      pre {
        max-width: 100%;
        overflow-x: auto;
        padding: 1rem;
        border: 1px solid #767676;
        border-radius: 0.3rem;
        background: #f5f7fa;
        font-size: 1rem;
        white-space: pre;
      }

      code {
        font-family: ui-monospace, monospace;
      }

      :focus-visible {
        outline: 3px solid #005ea8;
        outline-offset: 3px;
      }

      main:focus {
        outline: 3px solid #005ea8;
        outline-offset: 3px;
      }

      .skip-link {
        position: absolute;
        inset-inline-start: 1rem;
        top: 1rem;
        z-index: 10;
        transform: translateY(-200%);
        padding: 0.65rem 1rem;
        border: 2px solid #005ea8;
        color: #005ea8;
        background: white;
      }

      .skip-link:focus {
        transform: translateY(0);
      }

      [id] {
        scroll-margin-block-start: 1rem;
      }
    </style>
  </head>
  <body>
    <a class="skip-link" href="#main-content">
      跳至主要內容
    </a>

    <header>
      <nav aria-label="頁面區段">
        <ul>
          <li><a href="#instructions">操作指引</a></li>
          <li><a href="#example">程式碼範例</a></li>
        </ul>
      </nav>
    </header>

    <main id="main-content" tabindex="-1">
      <h1>有明確用途的鍵盤焦點</h1>

      <section aria-labelledby="instructions">
        <h2 id="instructions">操作指引</h2>
        <ol>
          <li>按 Tab 顯示跳過連結。</li>
          <li>啟動連結,移至主要內容。</li>
          <li>繼續按 Tab,移至程式碼區域。</li>
          <li>如果程式碼超出區域寬度,可使用方向鍵捲動。</li>
          <li>再次按 Tab,移至原生按鈕。</li>
        </ol>
        <p>
          主要區域可以取得焦點,
          但不是一般 Tab 停留點。
        </p>
      </section>

      <section aria-labelledby="example">
        <h2 id="example">可捲動程式碼範例</h2>

        <p id="code-help">
          這個區域可以取得鍵盤焦點,
          讓超出寬度的內容可以捲動。
          按 Tab 繼續移動。
        </p>

        <pre
          tabindex="0"
          role="region"
          aria-label="JavaScript 原始碼範例"
          aria-describedby="code-help"
        ><code translate="no">const message = "This deliberately long line demonstrates horizontal scrolling in a focusable code region.";
console.log(message);</code></pre>

        <button type="button" id="confirm-button">
          確認鍵盤操作
        </button>

        <p id="confirmation" role="status"></p>
      </section>
    </main>

    <script>
      const button = document.getElementById("confirm-button");
      const confirmation = document.getElementById("confirmation");

      button.addEventListener("click", () => {
        confirmation.textContent =
          "原生按鈕已啟動;此操作不會主動移動焦點。";
      });
    </script>
  </body>
  </html>

在這個範例中:

  • 跳過連結位於原始碼次序的最前方。
  • 主要區域使用 tabindex="-1" 作為焦點目標。
  • 可捲動程式碼區域使用 tabindex="0"。
  • 原生連結及按鈕不需要額外 tabindex。
  • 焦點提示保持可見。
  • 啟動按鈕會更新狀態訊息,不會主動移動焦點。
  • 不需要正值或自訂鍵盤啟動處理程式。

應使用目標瀏覽器及鍵盤設定測試,因為捲動及片段連結的焦點行為可能有所不同。

36.14 常見錯誤

錯誤較好的做法
使用正值重新排列網頁焦點次序修正原始碼及視覺次序
讓每個容器都成為 Tab 停留點只有在用途清晰時才加入焦點能力
將可取得焦點的 <div> 當成完整按鈕使用 <button>
以為 -1 會停用元素使用適當的停用或 inert 機制
移除焦點外框,卻沒有替代提示提供可見焦點提示
意外移動焦點將焦點變更與有意義的操作關聯
將焦點移至仍在螢幕範圍外的內容協調焦點及捲動
在 aria-hidden 區域內留下可取得焦點的內容移除焦點能力,或使用適當隱藏方式
使用移動式 tabindex,卻沒有鍵盤邏輯實作完整控制元件模式
任意使用 tabindex 修補模態介面使用適當對話框及焦點管理方式
只使用指標測試測試 Tab、Shift + Tab 及鍵盤啟動

本章總結: tabindex 管理焦點參與方式,不會提供完整互動行為。應優先使用原生控制元素,以 0 提供有明確用途的循序焦點,以 -1 提供受管理的焦點目標,並避免正值。保持合乎邏輯的次序、可見焦點及可預測的鍵盤互動。

Scroll to Top