回應式表格

大型資料表在小螢幕上往往難以閱讀,因為資料欄會溢出或縮小到無法辨識。本指南將示範如何使用固定位置,在捲動時保持標頭可見,以及如何在空間有限時,將表格轉換為適合行動裝置的「堆疊」版面配置。

回應式表格的核心,在於維持資料儲存格與標題之間的關係。

  1. 語意基礎:使用標準 <table> 元素搭配 <thead>、<tbody> 和 <th> 元素。
  2. 固定背景資訊:將 position: sticky 同時套用至欄標題和列標題。這樣一來,無論使用者往任何方向捲動多遠,都不會忘記資料代表的意義。
  3. Adaptive 轉換:使用 @container 查詢,而非 @media 查詢。這樣一來,表格就能根據自身寬度 (例如放在側欄或窄版資訊主頁小工具中) 調整,而不是根據整個可視區域。
  4. 標籤插入:在堆疊版面配置中,<thead> 會隱藏,並使用 ::before 虛擬元素和 CSS 變數,將無障礙標頭插入每個儲存格。

導入步驟

1. 標記和版面配置

使用標準語意標頭建構表格。使用 .table-wrapper 處理溢位,並為查詢提供容器。在容器中加入 tabindex="0" 屬性,確保只使用鍵盤的使用者能順利捲動表格。

為包裝函式提供 role="region" 屬性,以傳達其用途。使用 aria-labelledby (參照現有元素的 id 屬性,為表格加上標題/名稱) 或 aria-label 屬性,為表格提供無障礙元素名稱。

<div tabindex="0" role="region" aria-labelledby="employee-table-caption" class="table-wrapper">
  <table>
    <caption id="employee-table-caption">Current employee directory</caption>
    <thead>
      <tr>
        <th>Employee</th>
        <th>Role</th>
        <th>Status</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <!-- Row header remains sticky horizontally on desktop, 
             and becomes a sticky card header on mobile -->
        <th>Alex Rivera</th>
        <td>Engineer</td>
        <td>Active</td>
      </tr>
    </tbody>
  </table>
</div>

2. 設定固定標頭

啟用捲動功能並固定標頭。使用邏輯屬性和明確的 z-index 值,管理堆疊順序。

.table-wrapper {
  overflow: auto;
  max-inline-size: 100%;
  max-block-size: min(500px, 80vh);
  container-type: inline-size;
}

table {
  border-collapse: separate;
  border-spacing: 0;
}

/* Sticky column headers (desktop) */
thead {
  position: sticky;
  inset-block-start: 0;
  z-index: 1; /* Above sticky row headers */
}

/* Sticky row headers (desktop) */
tbody th {
  position: sticky;
  inset-inline-start: 0;
  background: white; /* Required to cover background during scroll */
}

3. 回應式堆疊版面配置

如果空間有限,請隱藏原始標題列,並將表格轉換為資訊卡。使用虛擬元素插入標籤。請只為非文字內容提供替代文字,否則螢幕閱讀器可能會同時朗讀文字內容和無障礙元素名稱。

@container (width < 600px) {
  table, thead, tbody, tr, th, td {
    display: block;
  }

  thead {
    /* MANDATORY: Hide column header from layout and screen readers. */
    display: none
  }

  tr {
    margin-block-end: 1.5rem;
    border: 1px solid #ccc;
  }

  /* Sticky row headers at the top of each block/card */
  tbody th {
    position: sticky;
    inset-block-start: 0;
    z-index: 2;
    background: #eee;
  }

  /* Define column labels as CSS variables (often set via JS) */
  table {
    --label-1: "Employee";
    --label-2: "Role";
    --label-3: "Status";
  }

  /* Inject labels */
  td:nth-child(2)::before {
    /* MANDATORY: If using non-text content, provide the accessible name (after the /) which ensures screen readers hear the text alternative. */
    content: var(--label-2) ": ";
  }
  /* MANDATORY: Map the label for each column */
  td:nth-child(3)::before {
    content: var(--label-3) ": ";
  }
}