Home/Learn/HTML, CSS & Responsive/Tables — Real Data, and Making It Fit a Phone

Tables — Real Data, and Making It Fit a Phone

Intermediate
Semantic HTML

A table is the right element for tabular data and the wrong one for layout. Headers with scope make it navigable; a phone needs a deliberate strategy, not a horizontal scrollbar by accident.

Overview

Tables carry an old reputation from the era when they were used for page layout, and the reaction — building data grids out of divs — throws away real functionality. A properly marked-up table lets a screen reader announce "Difficulty, Easy" as it moves across a row, so a blind user knows which column they are in. Div grids announce a stream of disconnected values. The genuine difficulty with tables is not semantics, it is that eight columns do not fit on a 360px screen, which needs a decision rather than an accident.

The Full Markup

caption, thead, th and scope — each one does a specific job for a screen reader.

caption, thead, th scope, tfoot
<table>
  <caption>Submissions this week</caption>

  <thead>
    <tr>
      <th scope="col">Problem</th>
      <th scope="col">Difficulty</th>
      <th scope="col">Runtime</th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <th scope="row">Two Sum</th>      <!-- the row's own header -->
      <td>Easy</td>
      <td>42 ms</td>
    </tr>
  </tbody>

  <tfoot><tr><td colspan="3">3 of 12 solved</td></tr></tfoot>
</table>

// scope="col" and scope="row" are what let a screen reader say
// "Two Sum, Difficulty, Easy" instead of just "Easy".

// caption is the table's accessible name, announced on entry.
// It also survives when the surrounding heading does not.

// Numbers align right and use tabular figures so columns line up:
td.num { text-align: right; font-variant-numeric: tabular-nums }

Tables on a Phone

Three strategies. Pick one deliberately; the default is none of them.

Scroll, stack, or prioritise
/* 1. Scroll horizontally — honest, keeps the table intact.
      Needs a focusable, labelled container or keyboard users
      cannot scroll it. */
<div class="table-wrap" tabindex="0" role="region" aria-label="Submissions">
  <table>…</table>
</div>
.table-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch }

/* 2. Stack into cards below a breakpoint — best for a few rows */
@media (max-width: 640px) {
  thead { display: none }
  tr { display: block; border: 1px solid var(--rule); margin-block: .5rem }
  td { display: flex; justify-content: space-between }
  td::before { content: attr(data-label); font-weight: 600 }
}
<td data-label="Difficulty">Easy</td>

/* 3. Hide low-priority columns, with a way to reveal them */
@media (max-width: 640px) { .col-runtime, .col-memory { display: none } }

/* Sticky header for a long table */
thead th { position: sticky; top: 0; background: var(--surface) }

When Not to Use a Table

And what to reach for instead.

Rows and columns whose intersection means something
// Not a table: page layout. Use grid or flexbox.
// Not a table: a list of cards. Use a ul.
// Not a table: a form's field alignment. Use grid.

// A table is right when the data has rows AND columns whose
// intersection means something. If you would export it to CSV,
// it is a table.

// If you must build a grid from divs — for virtualisation, say —
// you have to add every role by hand, and get them all right:
<div role="table" aria-rowcount="500">
  <div role="rowgroup">
    <div role="row"><span role="columnheader">Problem</span></div>
  </div>
  <div role="row"><span role="cell">Two Sum</span></div>
</div>
// This is a genuine reason to use a headless table library rather
// than reinventing it.

// Sortable columns must announce their state:
<th scope="col" aria-sort="ascending">
  <button>Runtime</button>
</th>

Key Points to Remember

  • 1scope="col" and scope="row" let a screen reader announce the header with each cell
  • 2caption is the table's accessible name and is announced when a user enters it
  • 3A horizontally scrolling table wrapper needs tabindex and a label, or keyboard users cannot scroll it
  • 4Stacking rows into cards with data-label pseudo-content is the usual small-screen alternative
  • 5Use a table when rows and columns intersect meaningfully; a div grid means writing every ARIA role by hand

Interview Questions

Sign in to ask Aria
1

What does the scope attribute do on a table header?

Medium
2

How would you make an eight-column table usable on a 360px screen?

Medium
3

When is a <table> the wrong element?

Easy

Ask Aria about Tables — Real Data, and Making It Fit a Phone

Your personal AI tutor — ask anything about this concept

Revision Status

Personal Notes

Sign in to save personal notes for this topic.

Discussion

Sign in to join the discussion.

Loading discussion…