Clean Markup

Structuring Accessible Data Tables with Caption Scope and Thead

Structuring Accessible Data Tables with Caption Scope and Thead

Early web developers famously overused <table> elements for page layout, which led some developers to avoid tables altogether—even when rendering real tabular data like pricing matrices, benchmark results, or schedules.

When displaying two-dimensional data, a semantic <table> is essential. However, a table made only of <tr> and <td> tags leaves screen-reader users unable to tell which column or row header belongs to a given cell.

Example

<!-- Avoid: Ambiguous table cells with no header associations -->
<table>
  <tr><td>Browser</td><td>Grid Support</td></tr>
  <tr><td>Chrome</td><td>Yes (v57+)</td></tr>
</table>

<!-- Prefer: Explicit caption, thead/tbody, and scope attributes -->
<table>
  <caption>CSS Grid Browser Support Matrix</caption>
  <thead>
    <tr>
      <th scope="col">Browser</th>
      <th scope="col">Grid Support</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th scope="row">Chrome</th>
      <td>Yes (v57+)</td>
    </tr>
  </tbody>
</table>

Input : Raw benchmark or feature comparison table
Output : Screen-reader friendly semantic HTML table with responsive horizontal scrolling.

Implementation

<div class="table-responsive" role="region" aria-labelledby="benchmark-caption" tabindex="0">
  <table class="data-table">
    <caption id="benchmark-caption">
      Algorithm Time Complexity Comparison
    </caption>
    <thead>
      <tr>
        <th scope="col">Algorithm</th>
        <th scope="col">Best Case</th>
        <th scope="col">Average Case</th>
        <th scope="col">Space Complexity</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <th scope="row">Two Sum (Hash Table)</th>
        <td><code>O(n)</code></td>
        <td><code>O(n)</code></td>
        <td><code>O(n)</code></td>
      </tr>
      <tr>
        <th scope="row">Binary Search</th>
        <td><code>O(1)</code></td>
        <td><code>O(log n)</code></td>
        <td><code>O(1)</code></td>
      </tr>
      <tr>
        <th scope="row">Bubble Sort</th>
        <td><code>O(n)</code></td>
        <td><code>O(n^2)</code></td>
        <td><code>O(1)</code></td>
      </tr>
    </tbody>
  </table>
</div>
.table-responsive {
  overflow-x: auto;
  border: 1px solid #dedede;
}

.data-table {
  width: 100%;
  border-collapse: collapse;
  text-align: left;
}

.data-table caption {
  font-weight: 700;
  font-size: 18px;
  padding: 14px 16px;
  text-align: left;
  color: #082840;
}

.data-table th,
.data-table td {
  padding: 12px 16px;
  border-top: 1px solid #dedede;
}

.data-table thead th {
  background-color: #f6f9fb;
  color: #082840;
}

Main Point: Using <th scope="col"> for column headers and <th scope="row"> for the first cell in each row causes screen readers to announce both the column name and row name as the user navigates across cells.

Credits

Photo by Carlos Muza

← Home