HTML Tables
A very common task in HTML is structuring tabular data, and there are a number of elements and attributes designed specifically for this purpose. Along with a bit of CSS for styling, HTML allows you to display tables with information, such as a tenant list or the schedule at the local cinema. In this section, we'll discuss everything you need to know about structuring tabular data with HTML.
Basic Elements of HTML Tables
A table is a structured set of data made up of rows and columns (tabular data). A table allows you to quickly and easily look up values that indicate some kind of connection between different types of data — for example, the type of a course, its duration, and its price:
When tables are properly built, even blind users can interpret tabular data from an HTML table.
For tables to be effective on the web, we need to provide some styling information using CSS, as well as a solid structure using HTML. In this module, we focus on the HTML part.
When Should You Not Use HTML Tables?
HTML tables should be used for tabular data — that's what they're designed for. Unfortunately, many developers used to rely on HTML tables to build web pages, for example a row for the header, a row containing content columns, a row for the site footer, etc. This method was commonly used because CSS support in browsers used to be terrible many years ago.
In short, using tables to build a website instead of CSS layout techniques is a bad idea. The main reasons are as follows:
- Using tables for site layout reduces accessibility for users with visual impairments. Screen readers used by blind users interpret the tags present in an HTML page, read and relay the content to the user. Because tables are not the right tool for layout, and the markup is more complex than with CSS layout techniques, screen reader results will be confusing for their users.
- As mentioned above, creating layouts generally involves more complex markup structures compared to CSS layout techniques. This can lead to code that is harder to write, maintain, and develop further.
- Tables are not automatically adaptable. When we use proper layout containers (such as header, section, article, or div), their width defaults to 100% of the parent element. On the other hand, tables are sized based on their content by default, so additional measures are needed for table layout styling to work effectively across a wide range of devices.
Creating a Table
Let's build a simple table together.
The content of every table is enclosed between these two tags: <table>...</table>.
The smallest container inside a table is a table cell, which is created using the <td> element (td stands for “table data”). Create a file named tabel.html and add the following lines between the <body>...</body> tags:
<table>
<td>Cell 1</td>
</table>
If we want a row with four cells, we need to copy the first cell three more times. Update your table content to look like this:
<table>
<td>Cell 1</td>
<td>Cell 2</td>
<td>Cell 3</td>
<td>Cell 4</td>
</table>
As you'll see, the cells are not placed one below the other, but automatically align side by side in the same row. Each td element creates a single cell, and together they form the first row. Every cell you add makes the row grow longer.
To stop this row from growing and start placing subsequent cells on a second row, we need to use the <tr> element (tr stands for “table row”). Let's update the table by wrapping the four cells we already created inside tr tags, like this:
<table>
<tr>
<td>Cell 1</td>
<td>Cell 2</td>
<td>Cell 3</td>
<td>Cell 4</td>
</tr>
</table>
Now let's create another row — each row must be wrapped in an additional tr element, with each cell enclosed in a td tag:
<table>
<tr>
<td>Row 1-Cell 1</td>
<td>Row 1-Cell 2</td>
<td>Row 1-Cell 3</td>
<td>Row 1-Cell 4</td>
</tr>
<tr>
<td>Row 2-Cell 1</td>
<td>Row 2-Cell 2</td>
<td>Row 2-Cell 3</td>
<td>Row 2-Cell 4</td>
</tr>
</table>
As mentioned earlier, we need minimal styling using CSS to make our table look better.
Copy and paste the code below into the head section of your tabel.html file:
<style>
html {
font-family: sans-serif;
}
table {
border-collapse: collapse;
border: 2px solid rgb(200,200,200);
letter-spacing: 1px;
font-size: 0.8rem;
}
td, th {
border: 1px solid rgb(190,190,190);
padding: 10px 20px;
}
th {
background-color: rgb(235,235,235);
}
td {
text-align: center;
}
caption {
padding: 10px;
}
</style>
Our table should now look like the one in the following example:
Adding Headers Using the <th> Element
Now let's turn our attention to table headers — special cells that go at the beginning of a row or column and define the type of data that row or column contains.
To mark table headers both visually and semantically, we use the <th> element (th stands for “table header”). It works similarly to the td element, except it generates a header cell instead of a regular one.
Headers are important because they make it easier to find the data we're looking for, and the design generally looks better.
Let's update our table like this:
<table>
<tr>
<td> </td>
<th>Vertical Header</th>
<th>Vertical Header</th>
<th>Vertical Header</th>
<th>Vertical Header</th>
</tr>
<tr>
<th>Horizontal Header</th>
<td>Row 1-Cell 1</td>
<td>Row 1-Cell 2</td>
<td>Row 1-Cell 3</td>
<td>Row 1-Cell 4</td>
</tr>
<tr>
<th>Horizontal Header</th>
<td>Row 2-Cell 1</td>
<td>Row 2-Cell 2</td>
<td>Row 2-Cell 3</td>
<td>Row 2-Cell 4</td>
</tr>
</table>
Our table should look like the one below:
Expanding Cells Across Multiple Rows and Columns
Sometimes we want cells to span across multiple rows or columns.
To do this, we use the colspan and rowspan attributes. Both accept unitless values. For example, colspan = "2" makes a cell span two columns.
Returning to our table, we'll remove one horizontal header and two vertical headers so the remaining ones can expand:
<table>
<tr>
<td> </td>
<th colspan="2">Vertical Header</th>
<th colspan="2">Vertical Header</th>
</tr>
<tr>
<th rowspan="2">Horizontal Header</th>
<td>Row 1-Cell 1</td>
<td>Row 1-Cell 2</td>
<td>Row 1-Cell 3</td>
<td>Row 1-Cell 4</td>
</tr>
<tr>
<td>Row 2-Cell 1</td>
<td>Row 2-Cell 2</td>
<td>Row 2-Cell 3</td>
<td>Row 2-Cell 4</td>
</tr>
</table>
Now, our table looks like this:
One last feature we'll discuss in this article before moving on
HTML has a method for defining styling information for an entire column of data — the <col> and <colgroup> elements. These exist because it can be a bit annoying and inefficient to specify styles for columns — generally, you have to apply styling to every td or th in the column or use a complex selector like nth-child().
Now, I'm going to color the first two cells of each vertical header (column) differently. To do this, I'll modify the table and add the following markup right after the opening table tag, like so:
<table>
<colgroup>
<col>
<col style="background-color: yellow;">
</colgroup>
<colgroup>
<col>
<col style="background-color: skyblue;">
</colgroup>
<tr>
<td> </td>
<th colspan="2">Vertical Header</th>
<th colspan="2">Vertical Header</th>
</tr>
<tr>
<th rowspan="2">Horizontal Header</th>
<td>Row 1-Cell 1</td>
<td>Row 1-Cell 2</td>
<td>Row 1-Cell 3</td>
<td>Row 1-Cell 4</td>
</tr>
<tr>
<td>Row 2-Cell 1</td>
<td>Row 2-Cell 2</td>
<td>Row 2-Cell 3</td>
<td>Row 2-Cell 4</td>
</tr>
</table>
After these changes, our table looks like this:
We can specify styling information just once on a col element. col elements are placed inside a colgroup container, right below the opening table tag.
Effectively, we define two “style columns,” each specifying styling information for a column. We don't apply styling to the first column, but we still need to include an empty col element — if we don't, the style will only be applied to the first column.
If we wanted to apply styling information to both columns, we could include just one col element with a span attribute, like this:
<colgroup>
<col style="background-color: yellow" span="2">
</colgroup>
Just like rowspan and colspan, span takes a unitless value that specifies the number of columns the style should apply to.
HTML Tables – Advanced Features and Accessibility
Let's now look at some more advanced features of HTML tables — such as captions, summaries, and grouping rows into head, body, and footer sections — as well as table accessibility for users with visual impairments.
You can give your table a title or description by placing it inside a <caption> element just below the opening <table> tag:
<table>
<caption>My table description</caption>
...
</table>
As you can tell from the example above, caption serves to provide a brief description of the table's content. This is helpful for all readers who want to quickly understand whether the table is useful to them as they scan the page — but especially for blind users. Instead of reading the entire table, they can read the caption to find out what the table is about and then decide whether to read it in more detail.
As the table becomes more complex in structure, it's helpful to use a more structural definition. We can do this using <thead>, <tfoot>, and <tbody>, which allow you to mark a header, a footer, and a section for the body of the table.
These elements do not make the table more accessible for screen reader users and do not result in visual improvements. However, they are very useful for the design and layout of the table, because they can be targeted with CSS.
These structural elements are used as follows:
- The <thead> element should wrap the part of the table that represents the header — usually the first row containing the column titles. If you use <col> / <colgroup> elements, the table header should appear just below them.
- The <tfoot> element should wrap the part of the table that represents the footer — this could be a final row with values summed from previous rows, for example. You can include the table footer at the bottom of the table as expected, or even right below the header (the browser will render it at the bottom of the table).
- The <tbody> element should wrap the remaining parts of the table content that are not in the header or footer. It will appear below the table header or sometimes below the footer, depending on how you choose to structure it.
☞ tbody is always included in every table by default, even if you don't add it in your code. To verify this, open the example above and inspect the HTML code using your browser's developer tools — you'll see that the browser has added this tag. It's a good idea to include it manually, because it gives you more control over the table's structure and styling.
Before we move on, let's take a look at how our table looks now, after adding the structural elements we just discussed:
Nested Tables
It is possible to insert a table inside another table, as long as the full structure is included — including the table element itself. In general, this is not recommended because it makes the markup more confusing and less accessible for screen reader users. In many cases, you can simply insert additional cells / rows / columns into the existing table instead.
In the example below, you can see a nested table:
Tables for Users with Visual Impairments
Reading plain text is not a problem, but interpreting a table can be a challenge for people with visual impairments. However, with proper markup, we can replace visual associations with programmatic ones.
Using Column and Row Headers
Screen readers will identify all headers (th) and use them to create programmatic associations between those headers and the cells they refer to. The combination of column and row headers will help identify and interpret the data in each cell, allowing screen reader users to understand the table similarly to how a sighted user would.
The scope Attribute
This attribute can be added to the th element to tell screen readers exactly which cells a particular header applies to — whether it's a header for the row it's in or for the column. This way, we can clearly define column headers like this:
<thead>
<tr>
<th scope="col">Date</th>
<th scope="col">Location</th>
<th scope="col">Cost</th>
</tr>
</thead>
And each row could have a header defined like this (if we add row headers as well as column headers):
<tr>
<th scope="row">Haircut</th>
<td>12/09</td>
<td>21</td>
</tr>
Screen readers will recognize this structured markup and allow users to read the entire column or row.
The scope Attribute Has Two Other Possible Values — colgroup and rowgroup
These are used for headers that span across multiple columns or rows. If we look at the table below, you'll see that the “Duration” cell sits above the “Day” and “Evening” cells. All of these cells should be marked as headers (th), but “Duration” is a title that sits above and defines the other two subpositions. Therefore, “Duration” should receive a colgroup scope attribute, while the others should use col. Likewise, “HTML/CSS” should be marked with a rowgroup attribute.
