Bảng thích ứng

Bảng dữ liệu lớn thường không đọc được trên màn hình nhỏ vì các cột bị tràn hoặc thu nhỏ quá mức. Hướng dẫn này minh hoạ cách sử dụng vị trí cố định để giữ cho tiêu đề luôn hiển thị trong khi cuộn và cách chuyển đổi bảng thành bố cục "xếp chồng" phù hợp với thiết bị di động khi không gian bị hạn chế.

Cốt lõi của một bảng thích ứng là duy trì mối quan hệ giữa các ô dữ liệu và tiêu đề của chúng.

  1. Nền tảng ngữ nghĩa: Sử dụng các phần tử <table> chuẩn với các phần tử <thead>, <tbody> và <th>.
  2. Bối cảnh cố định: Áp dụng position: sticky cho cả tiêu đề cột và tiêu đề hàng. Điều này đảm bảo rằng cho dù người dùng cuộn đến đâu theo bất kỳ hướng nào, họ cũng không bao giờ mất ngữ cảnh của nội dung mà dữ liệu thể hiện.
  3. Phép biến đổi thích ứng: Sử dụng truy vấn @container thay vì truy vấn @media. Điều này cho phép bảng điều chỉnh dựa trên chiều rộng của chính nó (ví dụ: khi được đặt trong thanh bên hoặc tiện ích trang tổng quan hẹp) thay vì toàn bộ khung nhìn.
  4. Chèn nhãn: Trong bố cục xếp chồng, <thead> bị ẩn và các tiêu đề dễ tiếp cận được chèn vào mỗi ô bằng cách sử dụng các phần tử giả ::before và biến CSS.

Các bước triển khai

1. Đánh dấu và bố cục

Cấu trúc bảng bằng các tiêu đề ngữ nghĩa chuẩn. Sử dụng .table-wrapper để xử lý tình trạng tràn và cung cấp vùng chứa cho các truy vấn. Thêm thuộc tính tabindex="0" vào vùng chứa để đảm bảo rằng những người chỉ sử dụng bàn phím có thể cuộn bảng một cách nhất quán.

Cung cấp cho trình bao bọc một thuộc tính role="region" để truyền đạt mục đích của trình bao bọc. Cung cấp cho bảng một tên thành phần hỗ trợ tiếp cận bằng cách sử dụng aria-labelledby (tham chiếu đến thuộc tính id của một phần tử hiện có chú thích/đặt tên cho bảng) hoặc thuộc tính 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. Định cấu hình tiêu đề cố định

Bật tính năng cuộn và đặt tiêu đề cố định. Sử dụng các thuộc tính logic và giá trị z-index rõ ràng để quản lý thứ tự xếp chồng.

.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. Bố cục xếp chồng thích ứng

Khi không gian bị hạn chế, hãy ẩn hàng tiêu đề ban đầu và chuyển đổi bảng thành thẻ. Chèn nhãn bằng phần tử giả. Chỉ cung cấp văn bản thay thế cho nội dung không phải dạng văn bản, nếu không, trình đọc màn hình có thể đọc cả nội dung văn bản và tên thành phần hỗ trợ tiếp cận.

@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) ": ";
  }
}