自适应表格

大型数据表在小屏幕上往往会变得难以阅读,因为列会溢出或缩小到无法辨认的程度。本指南演示了如何使用粘性定位在滚动时保持表头可见,以及如何在空间有限时将表格转换为适合移动设备的“堆叠”布局。

自适应表格的核心是保持数据单元格与其标题之间的关系。

  1. 语义基础:将标准 <table> 元素与 <thead>、<tbody> 和 <th> 元素结合使用。
  2. 固定上下文:将 position: sticky 同时应用于列标题和行标题。这样可以确保无论用户在任何方向上滚动多远,都不会丢失数据所代表的上下文。
  3. 自适应转换:使用 @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) ": ";
  }
}