InternotesSharing Web Development Techniques

Blossoms

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 of fixed
  • 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 on thead or tr; 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.