The Holy Grail: Pure CSS Scrolling Tables with Fixed Headers
For a recent project I needed a nice HTML table library to render a long table of data with fixed headers. Figuring there must be a million of such libraries, I started searching around. This would seem to be a simple thing, yet after a day of searching I still couldn’t find a good solution.
There are indeed many ways make fixed headers but every method has flaws. Some require all columns to be the same width. Some don’t handle horizontal scrolling when you have lots of columns. Some use Javascript to reposition the rows instead of native scrolling.
The W3C has really let us down here. Fixed headers on tabular data would seem to be an extremely common use case. I don’t understand why this isn’t just built into HTML 5.
So... after much research I decided to build my own system. I’ve come up with what I think is the best possible way using modern CSS standards like flex-box while keeping the markup as semantic as possible. It uses 100% native scrolling, with only a few compromises and one significant flaw. I’m hoping one of my intrepid readers can come up with a solution.
Separating the Header from the Body
Getting an element to scroll is easy: put it inside a container with overflow:scroll. Getting part of it to stay fixed is not. I tried many ways to isolate the top row of a table but none of them work reliably (overflow, position:relative, and others). Browsers just don’t seem to like splitting a table into pieces. So the first compromise is using two tables, one for the headers and one for the body.
Splitting the table half also means auto layout won’t work. Auto layout is when the width of a table column is based on the contents of each cell of the column. Since the table header is separate from the body any autosizing on the body won’t affect the header and the columns won’t line up. I think auto-layout is a bad idea anyway due to speed and non-determinism issues, so I’m okay not supporting it. We can still have different widths for each column but we must explicitly set the widths using CSS or Javascript.
For my initial attempt I made just the body scroll and put the header above it. Using flexbox I could give the header it’s minimum required space and allocate the rest to the body. This is better than older solutions that give the body a fixed percentage like 90% since that will vary based on the page size. This solution will work, but only if the table isn’t too wide. If there are more columns than will fit on the page the header and body will scroll horizontally, but separately. You’ll scroll the body left and the header will stay put. Hmm. The opposite of progress.
Nested Scrollpanes
Then I hit on an idea: the header and body will stay together horizontally as long as they don’t overflow. If I put them inside a div exactly wide enough to fit them, then make that outer div scroll then it will do the right thing. the trick is getting one div to scroll only horizontally and the other to scroll only vertically. This is hard to explain verbally so let’s look at some markup.
<div id="constrainer">
<div class="scrolltable">
<table class'header'>
<thead>
<th>foo</th>
<th>foo</th>
<th>foo</th>
</thead></table>
<div class="body">
<table>
<tbody>
<tr>
<td>foo</td>
<td>foo</td>
<td>foo</td>
<td>foo</td>
</tr>
… close all the tags
There is a container called scrolltable
which contains a table called header
with only a single row. scrolltable
has a second child, a div called body
, which contains the data table. It’s reasonably semantic and not too many extra divs.The whole thing is wrapped in another div called constrainer
, which exists only to constrain this demo a fixed height and width. constrainer
is styled with:
#constrainer {
height: 200px;
width: 200px;
}
We want scrolltable
to scroll only horizontally, so we can do that by setting the height
to 100%
and overflow-x
to scroll
.
.scrolltable {
overflow-x: scroll;
height: 100%;
}
We want the body to be exactly wide enough for it’s content and only scroll vertically. This requires the magic -webkit-fit-content
width value, then setting overflow-y
to scroll
.
.scrolltable > .body {
width: -webkit-fit-content;
overflow-y: scroll;
}
I want the header to use it’s normal height and give the rest of the vertical space to the body. This sort of space allocation is perfect for flex-box.
.scrolltable {
display: flex;
display: -webkit-flex;
flex-direction: column;
-webkit-flex-direction: column;
}
.scrolltable > .header { }
.scrolltable > .body {
flex: 1;
-webkit-flex: 1;
}
We don't have to set anything on the header since the default flex value is 0.
There’s only one thing left, to give the columns explicit widths.
th, td {
min-width: 150px;
}
Live Demo
Here’s what it looks like. I’ve added some visual styling to make the rows and columns prettier but these extra rules don’t affect the scrolling mechanism.
Click for live demo
Overall I’m pretty happy with it. There’s only two significant flaws: the use of a magic -webkit-fit-content
property and the vertical scrollbar is missing. Actually, the scrollbar is there but it’s inside of the outer scrollpane so you don’t see it unless you’ve scrolled all the way the right most column. However, given that everyone is moving to gesture based scrolling, I think it’s a reasonable tradeoff. This system could be adapted to have fixed headers on the left edge instead of the top, but I can’t see a way to have both edges fixed. The W3C really should solve this properly.
I've put the code on github. Please fork it and play around. Suggestions and improvements would be greatly appreciated.
Update
My friend Cooper showed me a slightly different formulation that has less boilerplate: just one div around a single table and no magic webkit properties. The downside is you have to set the width on the wrapper DIV and height on the TBODY. That's not a bad tradeoff for using only standard css rules. I've added his version to the repo too.
Posted May 23rd, 2015
Tagged: