Briefly
Sometimes, for simplicity of perception, content needs to be formatted as a table. A table consists of rows and columns and is intended for structuring data. Often, similar data is placed in tables. An example of a table from school years is a class journal. Each row is a student. The columns are dates. Opposite each surname, a grade for the lesson can be placed, which took place on a specific date.
In HTML, there is a set of semantic tags for creating tables:
<table>
<thead>
<tbody>
<tfoot>
<th>
<tr>
<td>
Example
Let's create a table with the top three positions in the top-250 best films:
<table> <thead> <tr> <th>Position</th> <th>Rating</th> <th>Movie Title</th> <th>Release Year</th> </tr> </thead> <tbody> <tr> <td>1</td> <td>9.1</td> <td>The Green Mile</td> <td>1999</td> </tr> <tr> <td>2</td> <td>9.1</td> <td>The Shawshank Redemption</td> <td>1994</td> </tr> <tr> <td>3</td> <td>8.6</td> <td>The Lord of the Rings: The Return of the King</td> <td>2003</td> </tr> </tbody></table>
<table> <thead> <tr> <th>Position</th> <th>Rating</th> <th>Movie Title</th> <th>Release Year</th> </tr> </thead> <tbody> <tr> <td>1</td> <td>9.1</td> <td>The Green Mile</td> <td>1999</td> </tr> <tr> <td>2</td> <td>9.1</td> <td>The Shawshank Redemption</td> <td>1994</td> </tr> <tr> <td>3</td> <td>8.6</td> <td>The Lord of the Rings: The Return of the King</td> <td>2003</td> </tr> </tbody> </table>
Structural Tags
We will create a table together and figure out the necessary tags and attributes as we go along.
<table>
The most important tag for creating a table is <table>
. This is where it all begins. It all ends here. When the browser encounters this tag in the markup, it understands that a table will follow.
This is a paired tag, inside which we will further layout rows and cells. For now, let's simply create our table.
<table></table>
<table> </table>
<tr>
Any table primarily consists of rows. To add rows to the table, use the paired tag <tr>
. As many rows as needed must be written <tr>
inside <table>
.
For now, let's add three lines to the table:
<table> <tr></tr> <tr></tr> <tr></tr></table>
<table> <tr></tr> <tr></tr> <tr></tr> </table>
“tr” stands for “table row”.
<td>
All the tags so far have only created the structure, but we haven't added any data. To create a cell for data, you need the paired tag <td>
. Write as many <td>
inside <tr>
as needed for the table cells.
Cells make up the columns. In HTML, there is no special tag for columns.
Cells can theoretically exist without <tr>
. They will line up in a row. To make new cells appear in a new row, we use <tr>
.
<table> <tr> <td>iPhone 12 Pro</td> <td>$999</td> </tr> <tr> <td>iPhone 12</td> <td>$799</td> </tr> <tr> <td>iPhone 12 mini</td> <td>$699</td> </tr></table>
<table> <tr> <td>iPhone 12 Pro</td> <td>$999</td> </tr> <tr> <td>iPhone 12</td> <td>$799</td> </tr> <tr> <td>iPhone 12 mini</td> <td>$699</td> </tr> </table>
“td” stands for “table data”.
Now our table has 3 rows. Each row has two cells. From these cells, two columns are formed.
In terms of data sense, it's basically clear what this table is about. But it's better to add headers for the columns to eliminate confusion.
In principle, we can use the familiar <tr>
and <td>
again:
<table> <tr> <td>Model</td> <td>Price</td> </tr> <tr> <td>iPhone 12 Pro</td> <td>$999</td> </tr> <tr> <td>iPhone 12</td> <td>$799</td> </tr> <tr> <td>iPhone 12 mini</td> <td>$699</td> </tr></table>
<table> <tr> <td>Model</td> <td>Price</td> </tr> <tr> <td>iPhone 12 Pro</td> <td>$999</td> </tr> <tr> <td>iPhone 12</td> <td>$799</td> </tr> <tr> <td>iPhone 12 mini</td> <td>$699</td> </tr> </table>
But in this case, the headers of the cells will not differ either visually or semantically from ordinary cells.
<th>
There is a special tag for header cells or rows called <th>
. Let's wrap the headers in this paired tag:
<table> <tr> <th>Model</th> <th>Price</th> </tr> <tr> <td>iPhone 12 Pro</td> <td>$999</td> </tr> <tr> <td>iPhone 12</td> <td>$799</td> </tr> <tr> <td>iPhone 12 mini</td> <td>$699</td> </tr></table>
<table> <tr> <th>Model</th> <th>Price</th> </tr> <tr> <td>iPhone 12 Pro</td> <td>$999</td> </tr> <tr> <td>iPhone 12</td> <td>$799</td> </tr> <tr> <td>iPhone 12 mini</td> <td>$699</td> </tr> </table>
Cells wrapped in the <th>
tag have default styles applied: the text becomes bold and is centered within the cell. This helps to visually separate the headers from the other data in the table.
"th" stands for "table header".
Logical Grouping Tags
There are also the tags <thead>
, <tbody>
, <tfoot>
, and <caption>
. It may seem that we already have a beautifully formatted table. Why complicate things?
First of all, it looks nice 😄
But seriously, these tags help to better read the markup of complex tables and separate the structural parts of the table from each other. For example: a complex header from the body with data, and all this from the summary in the footer.
Moreover, a properly formatted table can appear in search engines as a snippet.

<thead>
The <thead>
tag is responsible for the header of the table. Inside this tag, one or more rows with table headers can be placed. <thead>
should be placed in the markup immediately after the opening <table>
or after <caption>
, but strictly before <tbody>
and <tfoot>
.
<thead>
is semantically similar to the <header>
tag, but its “influence” extends within the table.
Having these tags in the markup is convenient from a styling perspective. You can apply styles to the entire block at once using selectors like thead
, tbody
, or table
(why not? 😏)
Let's add <thead>
to our table:
<table> <thead> <tr> <th>Model</th> <th>Price</th> </tr> </thead> <tr> <td>iPhone 12 Pro</td> <td>$999</td> </tr> <tr> <td>iPhone 12</td> <td>$799</td> </tr> <tr> <td>iPhone 12 mini</td> <td>$699</td> </tr></table>
<table> <thead> <tr> <th>Model</th> <th>Price</th> </tr> </thead> <tr> <td>iPhone 12 Pro</td> <td>$999</td> </tr> <tr> <td>iPhone 12</td> <td>$799</td> </tr> <tr> <td>iPhone 12 mini</td> <td>$699</td> </tr> </table>
<tbody>
This tag is designed for the main part of the table. Rows with data are placed inside it. You can use multiple <tbody>
within a table, thereby separating the data into separate blocks.
This tag is semantically similar to <main>
, but within the table.
Let's wrap all the iPhones in one <tbody>
and add a couple of Androids to show that there can be more than one block of data:
<table> <thead> <tr> <th>Model</th> <th>Price</th> </tr> </thead> <tbody> <tr> <td>iPhone 12 Pro</td> <td>$999</td> </tr> <tr> <td>iPhone 12</td> <td>$799</td> </tr> <tr> <td>iPhone 12 mini</td> <td>$699</td> </tr> </tbody> <tbody> <tr> <td>Xiaomi Mi 10</td> <td>$768</td> </tr> <tr> <td>Xiaomi Black Shark 3 128 Gb</td> <td>$529</td> </tr> </tbody></table>
<table> <thead> <tr> <th>Model</th> <th>Price</th> </tr> </thead> <tbody> <tr> <td>iPhone 12 Pro</td> <td>$999</td> </tr> <tr> <td>iPhone 12</td> <td>$799</td> </tr> <tr> <td>iPhone 12 mini</td> <td>$699</td> </tr> </tbody> <tbody> <tr> <td>Xiaomi Mi 10</td> <td>$768</td> </tr> <tr> <td>Xiaomi Black Shark 3 128 Gb</td> <td>$529</td> </tr> </tbody> </table>
<tfoot>
The <tfoot>
tag is needed for the "Total" row — a row containing a summary of the table data. There can be only one <tfoot>
block in a table.
The browser always renders <tfoot>
at the bottom of the table, even if this block appears in the markup not last (although this is not very logical).
If for some reason you didn't use <thead>
or <tbody>
in the table, the footer will be rendered after all <tr>
.
Semantically, this tag is similar to <footer>
, but within the table.
Let's add a row to our table with the average price of all phones:
<table> <thead> <tr> <th>Model</th> <th>Price</th> </tr> </thead> <tbody> <tr> <td>iPhone 12 Pro</td> <td>$999</td> </tr> <tr> <td>iPhone 12</td> <td>$799</td> </tr> <tr> <td>iPhone 12 mini</td> <td>$699</td> </tr> </tbody> <tbody> <tr> <td>Xiaomi Mi 10</td> <td>$768</td> </tr> <tr> <td>Xiaomi Black Shark 3 128 Gb</td> <td>$529</td> </tr> </tbody> <tfoot> <tr> <td>Average Price:</td> <td>$758.8</td> </tr> </tfoot></table>
<table> <thead> <tr> <th>Model</th> <th>Price</th> </tr> </thead> <tbody> <tr> <td>iPhone 12 Pro</td> <td>$999</td> </tr> <tr> <td>iPhone 12</td> <td>$799</td> </tr> <tr> <td>iPhone 12 mini</td> <td>$699</td> </tr> </tbody> <tbody> <tr> <td>Xiaomi Mi 10</td> <td>$768</td> </tr> <tr> <td>Xiaomi Black Shark 3 128 Gb</td> <td>$529</td> </tr> </tbody> <tfoot> <tr> <td>Average Price:</td> <td>$758.8</td> </tr> </tfoot> </table>
<caption>
If you need to label the table, to give it a definition, you can use the paired tag <caption>
. It contains general information about the table. More details in the article about <caption>
.
For our table, a description like "Prices for flagship models of iPhone and Xiaomi" would fit perfectly. Let's add it to the markup (part of the data omitted for brevity):
<table> <caption>Prices for flagship models of iPhone and Xiaomi</caption> <thead> <tr> <th>Model</th> <th>Price</th> </tr> </thead> <tbody> <!-- Data for iPhone --> </tbody> <tbody> <!-- Data for Xiaomi --> </tbody> <tfoot> <tr> <td>Average Price:</td> <td>$758.8</td> </tr> </tfoot></table>
<table> <caption>Prices for flagship models of iPhone and Xiaomi</caption> <thead> <tr> <th>Model</th> <th>Price</th> </tr> </thead> <tbody> <!-- Data for iPhone --> </tbody> <tbody> <!-- Data for Xiaomi --> </tbody> <tfoot> <tr> <td>Average Price:</td> <td>$758.8</td> </tr> </tfoot> </table>
Attributes
In addition to global attributes, when working with tables, the attributes colspan
and rowspan
can be very useful. Both attributes are intended for merging cells. colspan
is needed for merging cells from 2 or more columns, while rowspan
is for merging cells from 2 or more rows.
Let's divide the descriptions of phones by manufacturer and model. The manufacturer will repeat; we will merge the cells containing its name across all rows. We will add one header "Manufacturer," and in the first <tr>
of each <tbody>
— the model name, and we will apply the rowspan
attribute. Now these cells occupy space in 3 and 2 rows respectively.
<table> <caption>Prices for flagship models of iPhone and Xiaomi</caption> <thead> <tr> <th>Manufacturer</th> <th>Model</th> <th>Price</th> </tr> </thead> <tbody> <tr> <td rowspan="3">iPhone</td> <td>12 Pro</td> <td>$999</td> </tr> <tr> <td>12</td> <td>$799</td> </tr> <tr> <td>12 mini</td> <td>$699</td> </tr> </tbody> <tbody> <tr> <td rowspan="2">Xiaomi</td> <td>Mi 10</td> <td>$768</td> </tr> <tr> <td>Black Shark 3 128 Gb</td> <td>$529</td> </tr> </tbody> <tfoot> <tr> <td>Average Price:</td> <td>$758.8</td> </tr> </tfoot></table>
<table> <caption>Prices for flagship models of iPhone and Xiaomi</caption> <thead> <tr> <th>Manufacturer</th> <th>Model</th> <th>Price</th> </tr> </thead> <tbody> <tr> <td rowspan="3">iPhone</td> <td>12 Pro</td> <td>$999</td> </tr> <tr> <td>12</td> <td>$799</td> </tr> <tr> <td>12 mini</td> <td>$699</td> </tr> </tbody> <tbody> <tr> <td rowspan="2">Xiaomi</td> <td>Mi 10</td> <td>$768</td> </tr> <tr> <td>Black Shark 3 128 Gb</td> <td>$529</td> </tr> </tbody> <tfoot> <tr> <td>Average Price:</td> <td>$758.8</td> </tr> </tfoot> </table>
But now in the final row, the number of cells does not match the total number of columns in the table. Let's stretch the first cell across two columns:
<table> <caption>Prices for flagship models of iPhone and Xiaomi</caption> <thead> <tr> <th>Manufacturer</th> <th>Model</th> <th>Price</th> </tr> </thead> <tbody> <tr> <td rowspan="3">iPhone</td> <td>12 Pro</td> <td>$999</td> </tr> <tr> <td>12</td> <td>$799</td> </tr> <tr> <td>12 mini</td> <td>$699</td> </tr> </tbody> <tbody> <tr> <td rowspan="2">Xiaomi</td> <td>Mi 10</td> <td>$768</td> </tr> <tr> <td>Black Shark 3 128 Gb</td> <td>$529</td> </tr> </tbody> <tfoot> <tr> <td colspan="2">Average Price:</td> <td>$758.8</td> </tr> </tfoot></table>
<table> <caption>Prices for flagship models of iPhone and Xiaomi</caption> <thead> <tr> <th>Manufacturer</th> <th>Model</th> <th>Price</th> </tr> </thead> <tbody> <tr> <td rowspan="3">iPhone</td> <td>12 Pro</td> <td>$999</td> </tr> <tr> <td>12</td> <td>$799</td> </tr> <tr> <td>12 mini</td> <td>$699</td> </tr> </tbody> <tbody> <tr> <td rowspan="2">Xiaomi</td> <td>Mi 10</td> <td>$768</td> </tr> <tr> <td>Black Shark 3 128 Gb</td> <td>$529</td> </tr> </tbody> <tfoot> <tr> <td colspan="2">Average Price:</td> <td>$758.8</td> </tr> </tfoot> </table>
Tips
💡 A table has no built-in styles for displaying border cells. Don't be surprised if, after writing the markup, you don't see any borders. Use the CSS property border
.
You cannot set borders on the <tr>
, <thead>
, <tfoot>
, and <tbody>
elements, so set them on the <table>
, <th>
, or <td>
tags.
💡 Carefully count the number of cells in the table rows. They should be the same. It is especially important to do this when you are stretching cells horizontally or vertically. Don’t be surprised if at the bottom or on one side of the table there suddenly appears a cell, disrupting the beauty of your table. You may have forgotten to remove an extra cell somewhere.
💡 With CSS, you can create a structure visually resembling a table, but it’s better not to do that. It is important not only for visual similarity but also for semantic significance. The easiest way to ensure consistency between meaning and visual similarity is to use the tags from this article.
💡 The width of the table by default adapts to the content inside. If no additional CSS properties are specified, this can lead to certain difficulties on responsive sites. If the content does not fit in a small screen, the table does not shrink; it has a horizontal scroll.
There are several potential solutions to this problem: hide non-essential information for mobile users or restructure the table display, for example, using the display
property.
In practice
Advice 1
🛠 A common design technique is highlighting table rows every other line. This helps read long tables, giving your eyes something to grab onto.
For example, let’s make every second row have a brown background. This requires just one CSS rule with the pseudo-class :nth
:
tr:nth-child(odd) { background-color: #663613;}
tr:nth-child(odd) { background-color: #663613; }
Just in case, let’s play it safe and limit the area of coloring to just the body of the table, excluding the header and footer.
tbody tr:nth-child(odd) { background-color: #663613;}
tbody tr:nth-child(odd) { background-color: #663613; }
🛠 It’s possible to make the header row stick when scrolling through a long table. This is convenient if there is a lot of data, and the user can easily forget which data corresponds to which column.
This is quite easy to achieve using position
. Keep in mind that this property cannot be applied to <thead>
or <tr>
tags, so we will apply it to <th>
.
th { position: -webkit-sticky; position: sticky; top: 0; z-index: 1;}
th { position: -webkit-sticky; position: sticky; top: 0; z-index: 1; }
Don’t forget to add position
to the parent. At the same time, let’s make sure that only the headers in the table header are sticky.
table { position: relative;}thead th { position: sticky; position: -webkit-sticky; top: 0; z-index: 1;}
table { position: relative; } thead th { position: sticky; position: -webkit-sticky; top: 0; z-index: 1; }
Set a background for the headers so the text in the cells isn’t visible while scrolling. And to get rid of lines between cells, we’ll set the border
property for the entire table:
table { position: relative; border-collapse: collapse;}thead th { position: -webkit-sticky; position: sticky; top: 0; z-index: 1; background-color: #FF8630;}
table { position: relative; border-collapse: collapse; } thead th { position: -webkit-sticky; position: sticky; top: 0; z-index: 1; background-color: #FF8630; }