Table Sections: caption, thead, tbody, tfoot and colgroup

Row groups give a table a header, a body and a summary. They are what make long tables printable, scrollable and understandable.

Concept

Beyond rows and cells, a table can be divided into row groups and given a caption and column groups. None of these are decoration; each solves a specific problem that plain rows cannot.

ElementPurpose
captionThe name of the table, announced before it
colgroup / colApply width or styling down a whole column
theadThe header rows
tbodyThe data rows, and there may be several
tfootSummary rows such as totals

Order in the source

<table>
  <caption>...</caption>
  <colgroup>...</colgroup>
  <thead>...</thead>
  <tbody>...</tbody>
  <tfoot>...</tfoot>
</table>

The caption must be first. tfoot may be written either before or after tbody - older HTML required it before - and browsers render it at the bottom either way. Writing it last is clearer.

caption

<table>
  <caption>Fee structure by course and year, 2026 intake</caption>
  ...
</table>

The caption is the accessible name of the table. Screen reader users can list every table on a page; without a caption they see table, table, table. It is one line of markup and it is the difference between a usable page and a guessing game.

Style it, do not hide it. If the design has a heading above the table that says the same thing, use the caption and visually hide the heading rather than the other way round.

thead, tbody and tfoot

<table>
  <caption>Monthly rainfall, district stations</caption>
  <thead>
    <tr>
      <th scope="col">Station</th>
      <th scope="col">June</th>
      <th scope="col">July</th>
      <th scope="col">August</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th scope="row">Riverside</th>
      <td>184</td><td>312</td><td>276</td>
    </tr>
    <tr>
      <th scope="row">Hill Gate</th>
      <td>210</td><td>344</td><td>301</td>
    </tr>
  </tbody>
  <tfoot>
    <tr>
      <th scope="row">District total</th>
      <td>394</td><td>656</td><td>577</td>
    </tr>
  </tfoot>
</table>

What the groups buy you

  • Printing. When a long table spans several pages, browsers repeat the thead at the top of each one. Without it, page two is a wall of unlabelled numbers.
  • Scrolling. A sticky header is one CSS rule when the header rows are in a thead.
  • Sorting and scripting. A script can reorder the rows of tbody without touching the header or the totals.
  • Styling. Zebra striping, borders and alignment can target the body only.
thead th {
  position: sticky;
  top: 0;
  background: #f1f5f9;
}
tbody tr:nth-child(even) { background: #f8fafc; }
tfoot { font-weight: 600; border-top: 2px solid #94a3b8; }

Several tbody elements

A table may have more than one tbody, which is the clean way to group rows into sections.

<table>
  <caption>Courses by faculty</caption>
  <thead>
    <tr><th scope="col">Course</th><th scope="col">Duration</th></tr>
  </thead>

  <tbody>
    <tr><th colspan="2" scope="colgroup">Design</th></tr>
    <tr><td>Product design</td><td>3 years</td></tr>
    <tr><td>Communication design</td><td>3 years</td></tr>
  </tbody>

  <tbody>
    <tr><th colspan="2" scope="colgroup">Engineering</th></tr>
    <tr><td>Civil</td><td>4 years</td></tr>
  </tbody>
</table>

colgroup and col

Columns do not exist as elements, so colgroup is the only way to address one directly.

<table>
  <colgroup>
    <col style="width: 40%">
    <col span="3" class="numeric">
  </colgroup>
  ...
</table>

Only a small set of properties work here - width, background, border and visibility. Text alignment and padding do not inherit through a col, so those still need cell selectors. span lets one col cover several columns.

The practical value is column widths. Setting a width once beats repeating it on every cell.

Important rules

  • caption must be the first child of table.
  • colgroup comes after the caption and before any row group.
  • Only one thead and one tfoot per table; tbody may repeat.
  • Row groups do not change the visual layout by themselves; they add structure and hooks.
  • col is a void element.
  • Browsers insert an implicit tbody around loose rows, so the DOM often has one you did not write.

Common mistakes

  • Omitting the caption and putting a heading above the table instead.
  • Putting the caption after the rows.
  • Skipping thead on a long table, so printed pages lose their headers.
  • Expecting col to apply padding or text alignment.
  • Using tfoot for footnotes rather than for summary data.
  • Wrapping row groups in a div, which is invalid inside a table.

Best practices

  • Caption every table, and write it to be useful when read alone.
  • Use thead and tbody on every table, even short ones. It costs two lines.
  • Use tfoot for totals and averages.
  • Set column widths through colgroup.
  • Make the header sticky on long tables.
  • Split logical sections into separate tbody elements rather than styling a marker row.

Practice

  1. Add a caption, thead, tbody and tfoot to a plain table, then print it to PDF and check whether the header repeats.
  2. Make the header row sticky using two CSS declarations.
  3. Split a twenty row table into three tbody groups with a section heading row in each.
  4. Set the first column to thirty percent width with colgroup, then try to set its text alignment the same way and explain what happens.

Useful resources

Hand picked references for this topic
Written by Lorens Mishra

Software Engineer Notes Management System Administrator

Continue reading

All HTML notes →

Discussion

0 comments
Sign in to join the discussion.

No comments yet. Be the first to say something.