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>.
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?
<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?
How do I add borders to a table?
border: 1px solid #999 to th, td and border-collapse: collapse to the table so adjacent borders merge into single grid lines.