반응형 표

대규모 데이터 표는 열이 넘치거나 가독성을 넘어 축소되므로 작은 화면에서 읽을 수 없는 경우가 많습니다. 이 가이드에서는 고정 위치 지정을 사용하여 스크롤하는 동안 헤더를 계속 표시하는 방법과 공간이 제한된 경우 표를 모바일 친화적인 '누적' 레이아웃으로 변환하는 방법을 보여줍니다.

반응형 표의 핵심은 데이터 셀과 헤더 간의 관계를 유지하는 것입니다.

  1. 시맨틱 기반: <thead>, <tbody>, <th> 요소와 함께 표준 <table> 요소를 사용합니다.
  2. 고정 컨텍스트: 열 헤더와 행 헤더 모두에 position: sticky를 적용합니다. 이렇게 하면 사용자가 어느 방향으로든 스크롤해도 데이터가 나타내는 컨텍스트를 잃지 않습니다.
  3. 적응형 변환: @media 쿼리 대신 @container 쿼리를 사용합니다. 이렇게 하면 전체 표시 영역이 아닌 자체 너비 (예: 사이드바 또는 좁은 대시보드 위젯에 배치된 경우)에 따라 표가 조정됩니다.
  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) ": ";
  }
}