Skip to content
Foxipa

Suggest an edit

Tell us what should be corrected, clarified, or improved. Submission will be connected to Foxipa’s contribution service later.

No information is sent anywhere yet. Your suggestion is only prepared in your browser.

Learning

HTML Tables

Learn to build readable, accessible data tables: captions, headers with scope, row grouping, spanning cells, and advanced column grouping with colgroup and col.

Levels: Beginner ~22 min read

Basic

Tables get a bad reputation they do not deserve. What earned that reputation was a decade of table layouts in the 2000s, stretching a reporting element into a page scaffold. For actual tabular data, data where each row has the same kinds of values and each column means the same thing across rows, a table is not just the right choice. It is the only element that says what the content is.

Quick check

Complete the table with the element that marks a header cell.

<tr>
  < scope="col">Language</th>
</tr>

A screen reader inside a real table can tell the reader which row and column they are in, jump by column header, and announce the header again with every value so nothing is lost. Those powers come from structure you have to write, not from the grid of cells on screen.

The core table elements have distinct structural responsibilities.

Element

Role

<table>

The table structure as a whole

<caption>

Names/describes the table

<tr>

Defines a table row

<th>

Defines a header cell

<td>

Defines a data cell

<thead>

Groups header rows

<tbody>

Groups body rows

<tfoot>

Groups footer rows

The three elements every table starts with

A table is rows of cells. The <table> wraps everything, <tr> marks each row, and cells are either <th> headers or <td> data. Which one you use is a semantic decision with real consequences: a header cell announces itself and its associations, a data cell does not.

html basic.html
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<table>
  <tr>
    <th>Day</th>
    <th>Loaves baked</th>
    <th>Sold out by</th>
  </tr>
  <tr>
    <td>Thursday</td>
    <td>60</td>
    <td>13:00</td>
  </tr>
  <tr>
    <td>Friday</td>
    <td>75</td>
    <td>12:30</td>
  </tr>
</table>

The smallest honest table: one header row, two data rows.

Captions name the whole table

The <caption> element is the table's title, and it must be the first child of the table. It is announced before any cell, which gives readers the context to decide whether to dive in or skip past. A visible heading above a table does not have that guaranteed relationship, and an aria-label is invisible to the many sighted readers who scan page structure.

Good captions answer the question a reader asks before the first row: what is this a table of, and for what period or scope? Baking record, winter season beats Table 1 every time it is read aloud.

Scope tells cells which header owns them

Tables have two axes, so header cells need to say which one they label. That is the job of the scope attribute: scope="col" for column headers along the top, scope="row" for row headers down the side. Screen readers use it to announce the right header with each value, and it costs one attribute to write.

Many tables deserve both kinds. A schedule where the first column names the day works best with that column as row headers, so a reader moving across Friday's row hears Friday attached to every value along the way.

Grouping rows into head, body, and foot

Larger tables earn structure of their own: <thead> holds the header rows, <tbody> the data, and <tfoot> summary rows such as totals. Browsers keep a <thead> pinned when tables scroll in some contexts, and long tables print their header row on every page. The groups also give styles a clean place to hook without class soup on every row.

html grouped.html
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<table>
  <caption>Baking record, winter season</caption>
  <thead>
    <tr>
      <th scope="col">Day</th>
      <th scope="col">Baked</th>
      <th scope="col">Sold</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th scope="row">Thursday</th>
      <td>60</td>
      <td>58</td>
    </tr>
    <tr>
      <th scope="row">Friday</th>
      <td>75</td>
      <td>75</td>
    </tr>
  </tbody>
  <tfoot>
    <tr>
      <th scope="row">Weekend total</th>
      <td>135</td>
      <td>133</td>
    </tr>
  </tfoot>
</table>

A fully structured table: caption, header group, body, and a totals row.

Grouping and controlling columns

When a table needs column-wide structure, <colgroup> and <col> let you describe columns before the row data begins. <colgroup> represents a group of columns; its child <col> elements represent one or more columns inside that group. They belong directly inside the <table> after any <caption> and before the row groups.

There are two useful forms. A <colgroup> can contain <col> elements, which is useful when different columns need different treatment. Or, when it contains no <col> children, the group itself can use span to represent several adjacent columns. The specification limits that span to a positive integer up to 1000.

html column-groups.html
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<table>
  <caption>Quarterly sales by region</caption>
  <colgroup>
    <col class="region-column">
    <colgroup span="2" class="sales-columns"></colgroup>
  </colgroup>
  <thead>
    <tr>
      <th scope="col">Region</th>
      <th scope="col">Q1</th>
      <th scope="col">Q2</th>
    </tr>
  </thead>
  <tbody>
    <tr><th scope="row">North</th><td>$42k</td><td>$48k</td></tr>
    <tr><th scope="row">South</th><td>$39k</td><td>$45k</td></tr>
  </tbody>
</table>

A table can group columns before its header and data rows. The groups do not replace the visible header cells.

A <col> is empty: it does not contain cell content. Its job is to identify columns in the table model and give CSS a column-level hook where the table layout supports it. Do not put headings, labels, or data inside it. Also remember that column-wide styling is not a general way to style every CSS property on every cell; use selectors on the actual cells when you need ordinary cell styling.

Interactive Runnable example
Edit

Ready to run.

Quick check

Where does a colgroup belong in a table's structure?

<table>
  <caption>Sales</caption>
  
  <thead>...

Quick check

You need one column group to cover three adjacent columns and it contains no col elements. Which markup expresses that directly?

<table>
  
  <thead>...

Spanning cells across rows and columns

Occasionally data genuinely needs a cell to stretch: colspan="2" merges a cell across two columns, rowspan="2" down two rows. Both remove a cell from every row or column they absorb, which means the markup of following rows shifts to compensate. Sparse spanning is fine; heavy spanning produces a lattice that is hostile to screen readers and almost impossible to restyle.

The specification, translated

The HTML Standard describes a table as a grid of rows, columns, and cells. The important authoring idea is simpler: use a table when the meaning of one cell depends on its row and column. A table is not just a way to make boxes line up.

The table model also explains why the elements work as a family. <caption> names the table, <tr> creates rows, <th> and <td> create cells, and <thead>, <tbody>, and <tfoot> group rows. The browser can then expose those relationships to other software instead of leaving them as visual guesses.

The specification defines a table as a two-dimensional grid whose cells must cover the grid without overlapping. That is why colspan and rowspan are structural: they change how cells occupy the grid. Do not use them simply to make a visual heading or spacing row; use them when the data really spans multiple columns or rows.

For accessibility, W3C guidance adds the practical layer: a header needs a relationship to the cells it describes. Use <th> for headers, scope="col" or scope="row" when the direction is clear, and the more explicit headers/id relationship for genuinely complex multi-level tables. If a complex table can be split into simpler tables without losing meaning, that is often easier for everyone.

One subtle specification point is worth knowing: <caption> is part of the table structure, not a decorative heading placed above it with CSS. It belongs as the table's first child, which lets user agents and assistive technologies treat it as the table's caption.

A working table

The runnable example below is the bakery's weekend record, caption, scope attributes, and all. Try removing the scope attributes and nothing visibly changes, which is exactly the problem: the loss lands on someone using a screen reader. Try renaming the caption to something vague and notice how much context the table loses before its first row.

Interactive Runnable example
Edit

Ready to run.

Quick check

A table has both column headers and row headers. Which markup most directly tells the browser the direction of each header?

<th >Price</th>

Mistakes worth avoiding

  • Table layouts. A page scaffold is not row-and-column data, and grid and flexbox exist precisely so this habit can die.

  • Skipping scope because the table looks obvious on screen. Looks are local to one rendering; scope travels with the markup.

  • Replacing the caption with an aria-label, which hides the table's name from sighted readers scanning the page.

  • Splitting one logical table into several styled boxes. If the columns are shared, it is one table.

  • Using colspan to fake a title row above the headers. That is the caption's job, and it does it better.


Quick check

If you can explain what a screen reader gains from scope, say why the caption comes before the first row, and recognize when rowspan is fighting your content instead of serving it, you have this lesson. When you are ready, take the HTML Tables quiz.

Was this page helpful?

Mark this learning resource complete to track your learning progress.