HTML Tables

A <table> displays data in rows and columns, things like a price list, a schedule, or a comparison chart. Tables are for tabular data only. Don’t use a table to lay out a page, CSS Flexbox and Grid handle layout, and a layout table confuses screen readers trying to read it as data.

Creating a table

  • <table> wraps the whole table.
  • <tr> (table row) holds one row.
  • <td> (table data) holds one cell of data.
  • <th> (table header) marks a cell as a header for a row or column, instead of <td>.
<table>
  <tr>
    <th>Name</th>
    <th>Role</th>
  </tr>
  <tr>
    <td>Amina</td>
    <td>Engineer</td>
  </tr>
  <tr>
    <td>Priya</td>
    <td>Designer</td>
  </tr>
</table>

Caption

<caption> gives the table a title. It’s announced by screen readers right when they enter the table, which is more reliable than a heading placed above the table with no direct connection to it. Put it right after the opening <table> tag.

<table>
  <caption>Team roles, 2026</caption>
  <tr>
    <th>Name</th>
    <th>Role</th>
  </tr>
  <tr>
    <td>Amina</td>
    <td>Engineer</td>
  </tr>
</table>

Accessible headers with scope

The scope attribute on a <th> tells screen readers whether that header applies to a column or a row. This matters a lot on data-heavy tables, without it, a screen reader can only guess which header goes with a given cell.

<table>
  <caption>Quarterly revenue</caption>
  <tr>
    <th scope="col">Quarter</th>
    <th scope="col">Revenue</th>
  </tr>
  <tr>
    <th scope="row">Q1</th>
    <td>$12M</td>
  </tr>
  <tr>
    <th scope="row">Q2</th>
    <td>$16M</td>
  </tr>
</table>

Table sections: thead, tbody, tfoot

Larger tables can be split into a head, a body, and a footer. This groups rows semantically and gives you clear hooks for CSS.

<table>
  <caption>Quarterly revenue and costs</caption>
  <thead>
    <tr>
      <th scope="col">Quarter</th>
      <th scope="col">Revenue</th>
      <th scope="col">Costs</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th scope="row">Q1</th>
      <td>$12M</td>
      <td>$10M</td>
    </tr>
    <tr>
      <th scope="row">Q2</th>
      <td>$16M</td>
      <td>$2M</td>
    </tr>
  </tbody>
  <tfoot>
    <tr>
      <th scope="row">Total</th>
      <td>$28M</td>
      <td>$12M</td>
    </tr>
  </tfoot>
</table>

Spanning columns and rows

A cell can span multiple columns with colspan, or multiple rows with rowspan.

<table>
  <caption>Weekly schedule</caption>
  <tr>
    <th scope="col">Monday</th>
    <th scope="col">Tuesday</th>
    <th scope="col">Wednesday</th>
  </tr>
  <tr>
    <td colspan="2">Out of town</td>
    <td>Back in town</td>
  </tr>
</table>

Styling a table

Add borders and spacing with CSS, not the old HTML border attribute, which offers no control over colour, spacing, or style.

table {
  border-collapse: collapse;
  width: 100%;
}

th,
td {
  border: 1px solid #ccc;
  padding: 8px 12px;
  text-align: left;
}

Common mistakes

  • Using a table for page layout instead of actual data. Use CSS Grid or Flexbox for layout.
  • Leaving out <caption> and scope, which makes a table technically readable but hard to navigate with a screen reader.
  • Using <td> for header cells instead of <th>, losing the semantic distinction between headers and data.

FAQ

Do I need thead/tbody/tfoot on every table?

No, they’re most useful on larger tables. A small two or three row table works fine with just <tr> rows directly inside <table>.

How do I make a table scroll on small screens instead of squishing?

Wrap the table in a container and let that container scroll horizontally:

.table-wrapper {
  overflow-x: auto;
}
<div class="table-wrapper">
  <table>...</table>
</div>
  • Forms : another common data-entry element with its own accessibility rules
  • CSS Responsive Design : handling wide tables on small screens