ESC

Type to search the knowledge base.

Tables for Tabular Data

Use tables for data grids — th/scope, captions, headers, responsive strategies, and when CSS grid is the wrong substitute.

beginner3 min read
  • html
  • tables
  • a11y

HTML tables exist for tabular data: values where rows and columns both carry meaning. Using tables for page layout is a 1990s habit; skipping tables for real data grids and faking them with divs is a modern accessibility mistake.

Docs: MDN table basics, <table>, WAI tables.

Solid data table

<table>
  <caption>Q2 invoice status</caption>
  <thead>
    <tr>
      <th scope="col">Invoice</th>
      <th scope="col">Customer</th>
      <th scope="col">Amount</th>
      <th scope="col">Status</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th scope="row">INV-1842</th>
      <td>Globex</td>
      <td>$1,200.00</td>
      <td>Paid</td>
    </tr>
    <tr>
      <th scope="row">INV-1843</th>
      <td>Initech</td>
      <td>$840.00</td>
      <td>Open</td>
    </tr>
  </tbody>
</table>
  • caption — table title (visible or styled)
  • th scope="col|row" — associates headers for AT
  • thead / tbody / tfoot — structure for styling and semantics

Complex headers

<th scope="colgroup" colspan="2">2026</th>

For multi-level headers, use colgroup, headers/id associations when scopes aren’t enough — see WAI complex tables tutorial.

When not to use tables

Need Prefer
Page columns CSS Grid / Flex
Card layout Grid of articles
Definition pairs dl sometimes; or simple list
Spreadsheet-like data Table

Responsive strategies

.table-wrap {
  overflow-x: auto;
}

table {
  border-collapse: collapse;
  width: 100%;
  min-width: 36rem;
}

th,
td {
  padding: 0.5rem 0.75rem;
  text-align: start;
  border-bottom: 1px solid var(--border);
}

Options:

  1. Horizontal scroll wrapper
  2. Stacked row cards via CSS on small screens (careful with header repetition)
  3. Priority columns + “details” disclosure

Don’t display: block every cell without restoring header context for AT.

Interactive grids

Sorting, selection, and virtualization often need ARIA grid patterns or established component libraries. Start from a real <table> when the data is tabular; enhance with buttons in headers for sort:

<th scope="col">
  <button type="button" aria-pressed="false">
    Amount
  </button>
</th>

Interview out-loud

“Tables are for row/column data, not layout. I use caption, th with scope, and thead/tbody. Responsive tables usually scroll horizontally or reflow carefully while keeping header associations. Div-based ‘tables’ need heavy ARIA to catch up—prefer native.”

Footguns

  1. Layout tables.
  2. Missing headers on every column.
  3. Empty header cells without labels.
  4. CSS that breaks table semantics (display: block on tr without a tested pattern).
  5. Putting entire paragraph essays in one cell without structure.

Sortable header sketch

<table>
  <caption>Active users</caption>
  <thead>
    <tr>
      <th scope="col">
        <button type="button" aria-pressed="true">
          Name
          <span aria-hidden="true">▲</span>
        </button>
      </th>
      <th scope="col">
        <button type="button" aria-pressed="false">Last active</button>
      </th>
    </tr>
  </thead>
  <tbody>…</tbody>
</table>

Announce sort changes with a live region if the full table re-renders. Keep keyboard operability on the header buttons. Do not make the entire th a click-only div without a real button or proper grid pattern.

Empty and loading states

<table>
  <caption>Team members</caption>
  <thead>…</thead>
  <tbody>
    <tr>
      <td colspan="4">No members yet. <a href="/invite">Invite someone</a>.</td>
    </tr>
  </tbody>
</table>

Keep the table structure when empty so column headers still explain the data model. For loading, prefer skeleton rows with aria-busy on the table container rather than replacing the whole table with a spinner that loses context.

Numeric alignment

td.num {
  text-align: end;
  font-variant-numeric: tabular-nums;
}

Right-align numbers for comparison; keep headers aligned with their columns. Use scope and clear header text (“Amount (USD)”) so units are not only in CSS-generated content. For downloadable data, offer CSV alongside the table for power users and AT workflows that prefer raw data.

Further reading

Related guides