How-To

How-To · Tables

How to Create an HTML Table

Build a complete, accessible HTML table from scratch: start with the <table> wrapper, add a header row of <th> cells, fill in data rows of <td> cells, group them with <thead> and <tbody>, and title it with a <caption>.

XCODXHow-To · Updated

Overview

You need to present tabular data — a price list, a schedule, a comparison — as a proper HTML table with aligned columns and a labelled header. The recommended approach is to build the grid outward in layers: create the <table> wrapper, add one header row, add the data rows, then group and title them. Working in that order keeps the column counts consistent and the markup accessible from the first line.

Steps

1. Start with the <table> wrapper

Every table begins with a single <table> element. It is the frame that holds the whole grid; on its own it shows nothing, so treat this step as scaffolding you will fill in. Decide up front how many columns your data needs, because that number governs how many cells each row will carry. Keeping presentation out of the markup, you will add borders later with CSS rather than obsolete HTML attributes.

<table>
  <!-- rows will go here -->
</table>

2. Add a header row with <th> cells

Add the first <tr> and fill it with <th> header cells — one per column, naming each field. Using <th> rather than <td> here is what makes the table accessible: browsers render these cells bold and expose them to assistive technology as the labels for the values beneath. This header row also fixes the column count for the whole table, so give it exactly as many cells as your data has fields.

<tr>
  <th>Name</th>
  <th>Role</th>
  <th>Age</th>
</tr>

3. Add data rows with <td> cells

Now add one <tr> per record, filling each with <td> data cells in the same order as the headers. Every data row must carry the same number of cells as the header row, so each value lines up under the correct column. This is where the table earns its keep: with the headers set, each <td> value inherits the meaning of the column it sits in.

<tr>
  <td>Ada</td>
  <td>Engineer</td>
  <td>36</td>
</tr>

4. Group the rows with thead and tbody

With a header row and several data rows in place, wrap them by role: put the header row inside <thead> and the data rows inside <tbody>. This does not change how the grid looks, but it labels which rows are the heading and which are the body — improving accessibility and letting a long table repeat its header when printed across pages. It also makes the markup far easier to read and style.

<thead>
  <tr><th>Name</th><th>Role</th><th>Age</th></tr>
</thead>
<tbody>
  <tr><td>Ada</td><td>Engineer</td><td>36</td></tr>
</tbody>

5. Give the table a caption

Finally, add a <caption> as the very first child of <table>. This titles the whole grid and ties that title to the table programmatically, so a reader — or a screen-reader user moving between tables — knows exactly what the data represents. Place it inside the table, not in a separate paragraph nearby, because only inside does it count as the table's accessible name.

<table>
  <caption>Team members</caption>
  <!-- thead + tbody follow -->
</table>

Runnable example

Putting all five steps together — a complete, rendering table with a caption, grouped rows, and border CSS so every cell is visible:

The same recipe scales to real data. Here is a price list built the identical way — header row of <th>, body rows of <td>, grouped and captioned — rendering live so you can edit the cells:

When one heading should span several columns, give a <th> a colspan so a single cell caps a group of columns above the individual labels:

<thead>
  <tr>
    <th colspan="2">Name</th>
    <th>Age</th>
  </tr>
  <tr>
    <th>First</th>
    <th>Last</th>
    <th></th>
  </tr>
</thead>

And the full skeleton, all layers in place, ready to copy and fill with your own rows:

<table>
  <caption>Table title</caption>
  <thead>
    <tr>
      <th>Column one</th>
      <th>Column two</th>
      <th>Column three</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Row one, cell one</td>
      <td>Row one, cell two</td>
      <td>Row one, cell three</td>
    </tr>
    <tr>
      <td>Row two, cell one</td>
      <td>Row two, cell two</td>
      <td>Row two, cell three</td>
    </tr>
  </tbody>
</table>

The borders and spacing are all CSS, kept out of the markup:

table {
  border-collapse: collapse;
}
caption {
  font-weight: 700;
  text-align: left;
  padding-bottom: 0.5rem;
}
th,
td {
  border: 1px solid #999;
  padding: 6px 14px;
  text-align: left;
}
thead th {
  background: #4f46e5;
  color: #fff;
}
tbody tr:nth-child(even) {
  background: #eef2ff;
}
tbody tr:hover {
  background: #e0e7ff;
}

Common pitfalls

Do not reach for a table to lay out a page. Tables are for tabular data — records compared across rows and columns — and using them to position page elements makes the markup fragile and hard for assistive tech to read in order. When you just want a grid of inputs to submit rather than data to display, you are looking at HTML Forms instead. Copy more ready-made table patterns from the HTML table examples, and tweak any of them live in the editor.

Frequently asked questions

What is the minimum markup for an HTML table?
A <table> containing at least one <tr> row, and cells inside that row — <th> for a header or <td> for data. Everything else, including <thead>, <tbody> and <caption>, is optional structure that improves accessibility.
Do I need thead and tbody?
They are optional but recommended. They label which rows are the heading and which are the body, help assistive technology, and let a long table repeat its header when printed. They do not change the visible grid.
How do I add borders to a table?
With CSS, not HTML attributes: apply border: 1px solid #999 to th, td and border-collapse: collapse to the table so adjacent borders merge into single grid lines.