DarkSnow

Freelance Web Developer

DarkSnow

Fixed Table Headers

Fixed Table Headers

I’ve been working on a calendar app in Angular and decided that I needed a table which scrolls both horizontally and vertically at the same time.

Now, this in itself is easy enough to achieve. Just restrict the size of the <table> element and set scroll bars on it. Job done. My issue is that I also need a fixed header and a fixed first column. Being a fan of using CSS over JavaScript as much as possible I set about seeing if there was a way to make this work without JavaScript at all.

Being good programmers, let’s break this problem down into smaller chunks and see if we can solve them all. What do we actually need to do here?

Scrolly Table

The first item on this list is easy enough, just fix the size of the table element itself and make sure the contents are too big to fit. Setting overflow: auto; in CSS will then add scroll bars for us.

  Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec
1st              
2nd              
3rd              
4th              
5th              
6th              
7th              
8th              
9th              
10th              
11th              
12th              
13th              
14th              
15th              
16th              
17th              
18th              
19th              
20th              
21st              
22nd              
23rd              
24th              
25th              
26th              
27th              
28th              
29th              
30th              
31st              

This is a contrived example, clearly, but it does set the base sizing and styles for the rest of the examples. Here the entire table with all it’s headers and the first column are one big block, set inside the table, which has had display: block; set. Since that table is smaller than the content, by fixing the sizing, scroll bars appear. Simple but since our target is to keep the headers of both axis in view, not that useful.

Fixed header

Now, to add a fixed header to the table we make sure that the markup is correct and use a set of <thead> and <tbody> tags to separate the header from the table contents. This way we can scroll the body independently of the header, leaving it in place.

  Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec
1st              
2nd              
3rd              
4th              
5th              
6th              
7th              
8th              
9th              
10th              
11th              
12th              
13th              
14th              
15th              
16th              
17th              
18th              
19th              
20th              
21st              
22nd              
23rd              
24th              
25th              
26th              
27th              
28th              
29th              
30th              
31st              

Here we fix the height of the <tbody> instead of the <table> and in doing so we get the scroll bars we want by making sure the content is bigger than the containing <tbody>. Unfortunately, in order to make this work we need to set the <tbody> to <display: block> or similar and in doing so, we lose the automatic sizing of the table cells so the headers and data no longer line up. This can be fixed by explicitly setting the width of both <th> and <td> to exactly the same. A problem when sizing by ems and taking borders and spacing into account, not not insurmountable.

The bigger issue here is when there isn’t enough space for the table to sit in the browser window. As I’ve set the table to max-width: 100%; so it doesn’t push the entire page too wide in smaller windows, the scroll bars that appear completely knock the layout. Table cells scale with their content and everything ends up out of alignment.

In short, if you have a narrow table which will never go over the page width at any browser size, then this is a quick and easy solution, with the caveat of fixed width cells. For most cases though, I’d say this is completely broken.

Fixed first column

The first column is inside the <tbody> so the approach above can’t work. Perhaps that’s just as well given the other caveats that completely break that approach. Time to come up with something else for horizontally scrolling tables.

 
Jan
Feb
Mar
Apr
May
Jun
Jul
Aug
Sep
Oct
Nov
Dec
1st2nd3rd4th5th 6th7th8th9th10th 11th12th13th14th15th 16th17th18th19th20th 21st22nd23rd24th25th 26th27th28th29th30th 31st
                                     
                                     
                                     
                                     
                                     
                                     
                                     
                                     
                                     
                                     
                                     
                                     

Since the approach above only really works because the head and body are separate elements, it makes sense to apply the same here and restructure the table. By splitting out the column you want to fix you end up with two elements and can make the main content of the table scroll horizontally. This introduces the same issue as the fixed header, namely keeping the sizing consistent.

I’ve lined these up neatly using flexbox rather than any float options. Flexbox allows things to scale as required without any sort of hack and is just much easier for this sort of thing. The approach here allows the column widths to scale to their content, as tables do, just leaving the need to scale the height of rows, which I feel is an easier problem in most cases.

This approach is also seriously flawed though. It requires two separate tables so is not just a CSS layout solution. This breaking up of the table is not exactly semantic and therefore doesn’t sit well with me in principle. While the sizing issues are mitigated as mentioned above, they are still there so this is also not a great solution, but this does allow the first column to stay visible when the contents of the table are scrolled horizontally, so it does work.

Both axis, in principle

Now we’ve seen the individual parts of the problem. I’ve made a table scroll in both directions without fixed headers and by splitting parts of the table out I’ve made two tables with fixed axis, one scrolling horizontal and one scrolling vertically. While all this is in CSS there are some serious flaws with these approaches. The question is, how can we put them together.

It seems that what we need is to allow a section of the table to scroll in one direction and then have another container scroll in the other direction. That way can combine the two approaches, split the table up into parts and have them scroll independently while leaving their respective headers behind. This approach can never work though.

If I create a vertical scrolling section with a fixed header, the scroll bar appears to the far right as it is supposed to, as seen above. If I then create a second table for the vertical axis and have a container which scrolls horizontally around the table with the vertical scroll bar, what happens? Well, the vertical scroll bar will remain at the right of it’s container, where it should be. This means we’d have to scroll to the far right on the horizontal scroll bar before we even get to see the vertical scroll bar.

Having explored this I don’t think there is a CSS solution to this problem. But maybe that’s not a bad thing. I’m a big fan of limiting my use of JS as much as possible, preferring a CSS solution but in this case the CSS solutions I’ve come up with have flaws. They remove the innate ability of a table to adapt the cell size to it’s content and they require breaking up the table and distorting or obscuring it’s semantic meaning. If a little JS can handle the fixed headers and allow us to keep the inherent properties of an HTML table, then that’s a win.

With all that in mind, perhaps the first example is the right approach after all.

Putting it all together

Now that we’ve established that there is no pure CSS solution to this problem, here is what I came up with.

See the Pen Fixed Axis Table by Martin Fraser (@darksnow) on CodePen.

Here we get the best bits of using a table. As in the first example we just set the table element itself to scroll. That way the cells can scale to the content as needed, the table is just a semantic table with no extra markup and the container, with it’s scroll bars, will size to the view port, all responsive. This is the way I prefer to use HTML, nice a clean.

All that’s been added is a tiny bit of jQuery code in a scroll event handler. Since we want our axis labels to stick to the top and left side of the scrolling area, we just need to offset them by the amount the container has scrolled, in pixels. Since this is exactly what we get from scrollTop and scrollLeft properties of the element with the scroll bars, we don’t even need to calculate anything. We just set the offset in CSS and we’re done.

$(function() {
  $('#fixed-headers').scroll(function(ev) {
    $('thead').css('transform', 'translateY(' + this.scrollTop + 'px)');
    $('tbody th').css('transform', 'translateX(' + this.scrollLeft + 'px)');
  });
});

I’ve used a CSS3 transform here for two reasons. Firstly, if you want to use left and right to position the elements you need to set position: absolute; which removes the element from the document flow. Doing this means the table header is no longer part of the table and you lose the scaling. The other reason is performance. By using CSS3 transforms the browser can elect to use hardware acceleration to make the change. I won’t go into the full details here but essentially these capabilities are used for 3D transforms but by adding transform: translateZ(0) to the elements you will later target, the browser uses the available hardware acceleration.

In Conclusion [tl;dr]

While it is possible to cobble together various approaches to fix the first column, or the table header, even both at the same time, using pure CSS I think the compromises are too far. By keeping the HTML clean we can get the benefits of using the inherent capabilities of an HTML table and with a tiny bit of JavaScript we can still fix the headers inside a scrolling container.

I’ve implemented it here in jQuery to deal with cross browser issues and for clarity. In the real project I’m working on I’ve done the same thing using Angular. The JS you use doesn’t matter nearly as much as the markup and the approach. I do like using just CSS as much as I can but we need to be pragmatic on real projects so this small compromise is, I feel, the best approach.