What you'll learn
- Quick answer
- What is an HTML table (and when to use one)
- The building blocks: table, thead, tbody, tr, th, td
- Build a real data table
- Merging cells with colspan and rowspan
- Making tables accessible: caption and scope
- Styling: borders and zebra stripes with CSS
- Common mistakes to avoid
- Wrap-up and next steps
- FAQ
Quick Answer
An HTML table shows data in rows and columns using the <table> element. You wrap header cells in <thead> and data cells in <tbody>, use <tr> for each row, <th> for headings, and <td> for data. Add a <caption> and scope attributes for accessibility, and use CSS for borders and zebra stripes. Only use tables for real tabular data, never for page layout.
What is an HTML table (and when to use one)
An HTML table displays information in rows and columns, exactly like a spreadsheet or a printed marksheet. If your data has a natural grid shape, a price list, a class timetable, exam results, a feature comparison, a table is the correct, meaningful way to show it.
Here is the one rule that trips up beginners: use tables for data, not for layout. Years ago people built entire web pages using tables to position things. That is now considered wrong. Screen readers announce a table as a data grid, so using one for layout confuses users who rely on assistive technology. For page layout, use CSS (Flexbox or Grid). For rows and columns of related data, use a table.
Every HTML table is built from a small set of tags. In this tutorial you will learn all of them, add accessibility features, and finish with clean CSS styling. If you want the full foundation alongside this, our free HTML course walks through each element step by step.
The building blocks: table, thead, tbody, tr, th, td
Six elements do almost all the work. Here is what each one means:
<table>wraps the whole thing.<thead>groups the header row(s), the column titles.<tbody>groups the body, the actual data rows.<tr>is a table row ("tr" = table row). Everything lives inside a row.<th>is a header cell ("th" = table header). Browsers make it bold and centred by default.<td>is a data cell ("td" = table data), an ordinary cell.
A minimal table looks like this:
<table>
<thead>
<tr>
<th>Name</th>
<th>City</th>
</tr>
</thead>
<tbody>
<tr>
<td>Aarav</td>
<td>Pune</td>
</tr>
</tbody>
</table>The <thead> and <tbody> wrappers are optional for the browser to draw the table, but you should always include them. They make your markup easier to read, let you style headers separately, and help screen readers separate titles from data. Think of them as free structure.
When should you use <th> versus <td>? Use <th> for any cell that labels a row or column; use <td> for the values.
| Cell type | Purpose | Bold by default |
|---|---|---|
<th> | Labels a row or column | Yes |
<td> | Holds a data value | No |
Build a real data table
Let's put it together with a table that means something: a term-one marksheet. Notice two things. First, the leftmost cell of each body row is a <th>, not a <td>, because the roll number identifies that row. Second, we count the number of cells in every row so the columns line up.
<table>
<caption>Class 10 – Term 1 Results</caption>
<thead>
<tr>
<th>Roll No</th>
<th>Name</th>
<th>Maths</th>
<th>Science</th>
<th>Total</th>
</tr>
</thead>
<tbody>
<tr>
<th>1</th>
<td>Aarav</td>
<td>88</td>
<td>91</td>
<td>179</td>
</tr>
<tr>
<th>2</th>
<td>Diya</td>
<td>95</td>
<td>89</td>
<td>184</td>
</tr>
</tbody>
</table>This renders as a five-column grid with a bold header row on top. Each body row has one header cell (the roll number) and four data cells. Because the cell counts match across every row, nothing shifts out of alignment. That is the whole trick with tables: keep the columns consistent, and the browser does the rest.
Merging cells with colspan and rowspan
Sometimes one cell needs to cover several columns or several rows. Two attributes handle this:
colspanstretches a cell across columns (left to right).rowspanstretches a cell down rows (top to bottom).
Here is a timetable where a heading spans two columns and a day label spans two rows:
<table>
<thead>
<tr>
<th>Day</th>
<th colspan="2">Sessions</th>
</tr>
<tr>
<th></th>
<th>Morning</th>
<th>Evening</th>
</tr>
</thead>
<tbody>
<tr>
<th rowspan="2">Monday</th>
<td>HTML</td>
<td>CSS</td>
</tr>
<tr>
<td>JavaScript</td>
<td>Git</td>
</tr>
</tbody>
</table>The key gotcha: when a cell spans, the cells it covers must be removed from the rows below or beside it, not left empty. In the example above, the second Monday row has only two <td> cells because "Monday" already fills the first column via rowspan="2". If you accidentally add an extra cell, that row will bulge out by one column. When a merged table looks broken, count the cells in each row first, that is almost always the cause.
Recommendation: reach for colspan and rowspan only when the data truly needs it. Heavy merging makes tables harder to read and harder for screen readers to follow, so keep it minimal.
Making tables accessible: caption and scope
A sighted person scans a table and instantly sees which numbers belong to which row and column. A person using a screen reader cannot see that grid, so we give the browser extra hints. Two features do most of the work.
caption
The <caption> is the table's title. Put it as the very first child of <table>. It is read out before the data, so the user knows what the table is about, and it is better than a plain heading above the table because it is programmatically tied to the table.
<table>
<caption>Monthly fee schedule</caption>
<!-- rows go here -->
</table>scope
The scope attribute tells assistive tech whether a header labels a column or a row. Use scope="col" on column headers and scope="row" on row headers. This lets a screen reader announce, for example, "Maths, Aarav, 88" so the value always has context.
<thead>
<tr>
<th scope="col">Roll No</th>
<th scope="col">Name</th>
<th scope="col">Total</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">1</th>
<td>Aarav</td>
<td>179</td>
</tr>
</tbody>Adding scope takes seconds and costs nothing visually, so make it a habit on every real data table. It is one of the easiest accessibility wins in all of HTML.
Styling: borders and zebra stripes with CSS
A raw table has no borders and looks cramped. A little CSS makes it clean and readable. Start with borders and spacing:
table {
border-collapse: collapse;
width: 100%;
}
th,
td {
border: 1px solid #ccc;
padding: 8px 12px;
text-align: left;
}The most important line is border-collapse: collapse;. By default, every cell draws its own border, giving you ugly double lines with gaps between them. collapse merges adjacent borders into single crisp lines. The padding gives cells breathing room so text is not glued to the border.
Next, add a styled header and "zebra striping", shading every other row so the eye can track across a wide table:
thead th {
background: #1f2937;
color: #fff;
}
tbody tr:nth-child(even) {
background: #f3f4f6;
}The :nth-child(even) selector matches the 2nd, 4th, 6th row, and so on, and gives each a light grey background. Switch to odd if you prefer the striping to start on the first row. That is genuinely all zebra striping is, no JavaScript needed.
To learn selectors like :nth-child in depth, our free CSS course covers them with plenty of practice.
Common mistakes to avoid
A few errors show up again and again for beginners. Watch for these:
- Using tables for layout. If the content is not a grid of related data, use CSS Grid or Flexbox instead.
- Mismatched cell counts. Every row must add up to the same number of columns once you account for
colspanandrowspan. Uneven rows are the number one cause of broken-looking tables. - Skipping
<thead>and<tbody>. The table may still render, but you lose structure, easy styling, and accessibility. - Bolding header text with
<strong>instead of using<th>. A<th>is already a heading and carries meaning; faking it with a bold<td>does not. - Forgetting
captionandscope. They take moments to add and make your table usable for everyone. - Putting text directly inside
<table>or<tr>. Text belongs inside<td>or<th>only.
Wrap-up and next steps
You now have everything needed to build clean, accessible HTML tables. To recap the recommended pattern: wrap the table, add a <caption>, group headers in <thead> and data in <tbody>, use <th scope="col"> and <th scope="row"> for labels, <td> for values, and reach for colspan or rowspan only when the data genuinely needs merged cells. Finish with border-collapse: collapse, some padding, and an :nth-child stripe.
The best way to lock this in is to build one. Try recreating your own class timetable or a small price list from scratch, then style it. When it renders correctly and reads well in a screen reader, you have understood tables properly.
Ready for more? Keep going with the free HTML course on Priodemy, then layer on styling with the CSS lessons. Everything is free, forever.
Frequently Asked Questions
Do I really need thead and tbody in an HTML table?
The browser will still draw a table without them, so they are technically optional. But you should always include them. They separate your header from your data, let you style the two groups independently in CSS, and give screen readers clear structure. They cost nothing and make your markup cleaner, so treat them as required in practice.
What is the difference between th and td?
Use <th> for a cell that labels a row or column, such as a column title or a roll number. Use <td> for the actual data values. Browsers make <th> bold and centred by default, and it carries meaning for accessibility, so do not fake a header with a bold <td>.
How do colspan and rowspan work?
colspan stretches a cell across multiple columns, and rowspan stretches it down multiple rows. The important part is that the cells the merged cell covers must be removed from the neighbouring rows, not left empty. If a merged table looks broken, count the cells in each row, an extra or missing cell is almost always the cause.
Why should I add caption and scope to a table?
They make your table accessible. The <caption> gives the table a title that screen readers announce first, and the scope attribute tells assistive tech whether a header labels a column (scope="col") or a row (scope="row"). Together they let a screen reader read each value with its correct context. Both take seconds to add.
How do I add zebra stripes to an HTML table?
Use pure CSS, no JavaScript needed. Add tbody tr:nth-child(even) { background: #f3f4f6; } to shade every second row. Change even to odd if you want the striping to start on the first row. Pair it with border-collapse: collapse and some cell padding for a clean look.
Should I ever use tables for page layout?
No. Tables are for tabular data only, rows and columns of related information like results or price lists. Using them for layout confuses screen readers, which announce the content as a data grid. For arranging page elements, use CSS Flexbox or Grid instead.
