Fixed Table Headers
Simple HTML tables can fit well enough on a browser page, but there are times when longer tables can be a pain. Of course, you might ask yourself whether a long table belongs on a web page, but if it does, then it would make sense to make it more manageable.
This article explores two options for creating a table with a fixed header row and a scrollable body.
There is some good news and some bad news. The good news is that we can do this with a minimal amount of CSS in modern browsers.
You have probably guessed that the bad news involves Legacy Browsers. However, with a bit more work, we can achieve this.
The two methods will be:
- Use CSS Flex Box and virtually split the table into two parts, one of which will be fixed and the other of which will scroll
- Use the new
position:sticky
property
Sample Table
A simplified version of the sample table is:
<div id="table-container">
<table id="fixed">
<thead>
<tr><th>A</th><th>B</th><th>C</th><th>D</th></tr>
</thead>
<tbody>
<tr><td>apple</td><td>banana</td><td>cherry</td><td>date</td></tr>
<tr><td>apple</td><td>banana</td><td>cherry</td><td>date</td></tr>
</tbody>
</table>
</div>
For our purposes, not the following:
- The table is contained inside a container called
div#table-container
; this is only necessary for the second method - The table itself has in
id
offixed
- The table is properly structured and includes a specific header row inside the
thead
section - In real life there will be many more rows in the
tbody
section
The idea will be to fix the header row in place, and allow the body to scroll.
Using Flex Box
To achieve the effect it self is relatively simple. Most of the effort will be to make the columns look right.
Fixing the Header
The normal behaviour of a table is to look after the sections, rows and columns automatically. Unfortunately, this also includes resizing the table to fit the contents. We will need to change this by changing the display
propoerty:
table#fixed {
display: flex;
flex-direction: column;
}
Apart from breaking the columns, you won’t see anything useful. However, what it has achieved is to treat the two inner elements, thead
and tbody
as two separate entities, which is why they have their own column widths.
Limiting the tbody
The next step is to limit the height of the tbody
element. We could do this with:
table#fixed>tbody {
height: 140px;
}
You may or may not see an effect.
If you have visible borders around either the table or the containing div, you will see that the height has indeed been set, but that the contents has spilt out. This is the normal behaviour of a container, and it is call its overflow
property.
To get it working properly, you will need to change the tbody
’s overflow
property to contain spillage:
table#fixed>tbody {
overflow-y: scroll;
}
table#fixed>tbody {
height: 140px;
}
The overflow-y: scroll
property cause the tbody
not to show content that doesn’t fit, but to enable a vertical scroll bar for that content.
Apart from the columns looking wrong, we have achieved the result.
Fixing the Columns
Because the thead
and tbody
are independent, they have their own ideas of what the column widths should be. To fix this, we will have to take control.
There are many ways to set column widths, but using Flex Box allows the sort of flexibility which is normal in tables.
The first step is to set all the rows to use Flex:
table#fixed tr {
display: flex;
flex-direction: row;
}
You won’t see anything yet. Note that the flex-direction: row
property is redundant as that is the default; it is included only to make the intention clear.
The next part is a bit tedious:
table#fixed>thead>tr>th:nth-child(1),
table#fixed>tbody>tr>td:nth-child(1) {
max-width: 25%;
}
This means that both the first cell in both the thead
and the tbody
will get the property flex: 1
. You don’t have to qualify the selector quite so specifically. The following will also work:
table#fixed thead th:nth-child(1),
table#fixed tbody td:nth-child(1) {
max-width: 25%;
}
The point is to include the first cell in both the the thead
and the tbody
.
The important part is that the value for both should be the same.
Column Numbers
Of course, you may not have four columns. In this case, you would need to adjust the width
property accordingly.
Alternatively, you can use the flex
property:
table#fixed>thead>tr>th:nth-child(1),
table#fixed>tbody>tr>td:nth-child(1) {
flex: 1;
}
The flex: 1
property means that it will occupy one part of the total of the row. You can also vary this to take up a larger portion of the space.
You can do the same for the rest:
table#fixed>thead>tr>th:nth-child(2),
table#fixed>tbody>tr>td:nth-child(2) {
flex: 1;
}
table#fixed>thead>tr>th:nth-child(3),
table#fixed>tbody>tr>td:nth-child(3) {
flex: 1;
}
table#fixed>thead>tr>th:nth-child(4),
table#fixed>tbody>tr>td:nth-child(4) {
flex: 1;
}
Of course, if you really mean them all to be the same, you could have used:
table#fixed>thead>tr>th:nth-child(1),
table#fixed>tbody>tr>td:nth-child(1),
table#fixed>thead>tr>th:nth-child(2),
table#fixed>tbody>tr>td:nth-child(2),
table#fixed>thead>tr>th:nth-child(3),
table#fixed>tbody>tr>td:nth-child(3),
table#fixed>thead>tr>th:nth-child(4),
table#fixed>tbody>tr>td:nth-child(4) {
flex: 1;
}
Data Width
The problem with using either the width
property or the flex
property is what happens if the data is too wide for the column:
- In the case of
max-width
the column will fit, but the data will spill over the edge - In the case of
flex
the column will readjust to fit, which is what it is supposed to do
The problem is that the data in the tbody
and in the thead
may vary, so if the data is too wide for either column, you will get a discrepancy.
At this stage, you will just need to be careful.
The Scroll Bar
You will have noticed that the header columns are slightly wider than the body columns. This is because the body has a scroll bar, while the header does not. To fix this, we install a dummy scroll bar for the header:
table#fixed>thead, /* Dummy Scroll Bar */
table#fixed>tbody { /* Real Scroll Bar */
overflow-y: scroll;
}
The appearance will still be slightly odd: the scroll bar appears to extend to the top of the table, though you can only scroll the lower part.
Using position: sticky
The position: sticky
property is a relatively new feature, and is available on all modern browsers, with the predictable exception of Microsoft Internet Explorer. However, this is the end of the good news.
Now for the bad news:
- Safari still requires the
-webkit-
prefix, which is not so bad - Microsoft & Edge don’t support
position: sticky
onthead
ortr
; fortunately there is a workaround
The position: sticky
property locks an element from scrolling. Although you can use this to fix an element on the screen, you can also fix an element inside a container element, assuming that the rest of the contents would be scrollable.
Preliminary CSS
In the sample table, we have wrapped the table inside a div
element. This was ignored in the previous version, but is required in this case.
div#table-container {
height: 200px;
overflow-y: scroll;
}
This fixes the size of the container div
, suppresses the spillage, and allows scrolling.
table#fixed {
width: 100%;
}
To maintain the appearance, we get our table to fill the width of the container div
. We can make adjustments to the div
if we like.
Fixing the thead
In Firefox and Safari, you can add the following:
table#fixed thead {
position: sticky; // Standard, including Firefox
-webkit-position: sticky; // Safari
top: 0;
}
That’s it.
The position: sticky
property will lock the header in place. The top: 0
property will ensure that it is locked at the top of the block.
Apart from its simplicity, the best part is the table is never broken up, so we don’t have to worry about fixing the column widths.
Chrome & Edge
As stated before, position: sticky
doesn’t work with thead
or tr
. It does, however, work with th
and td
. This is slightly risky, as the thead
is not the only element which might contain headers.
Given this, we can use:
table#fixed>thead th {
position: sticky;
top: 0;
}
This allows the actual thead
to wander off with the scroll, but fixes its cells in place.
This will work, but in the sample, you won’t see it properly. This is because the colour of the cells is white, but the background colour was applied to the thead
, which is disappearing as we scroll. To fix this, we will need to apply the background colour to the cells:
table#fixed>thead th {
position: sticky;
top: 0;
background-color: #666;
}
Compatibility
To begin with, there is no point in fixing both the thead
and the contained cells. If you require cross-browser support, you may as well use the Chrome/Edge workaround above, as it also works with Firefox and Safari. One day, we will be able to use the first method on all browsers.
Having said this, this technique is not available with Internet Explorer, which, unfortunately, still hangs on as a supported browser. If you need this additional support you can use the Flex Box method and come back in a few years to this one.