Tables with a fixed header row, self-understood for desktop-applications, are not implemented by web browsers. We developers need to implement this. Hard to understand, but a fact. In a passed Blog I showed a minimalistic way to do that. Of course it can be done in many different ways, using CSS and JavaScript. Most people would download some libraries to find something suitable. This Blog tries to summarize ways to do it without library.
WARNING: I do not consider special browsers with proprietary interests, I write about main stream browsers that conform to w3c and whatwg standards. So the source code I present here may not work for any browser.
The Problem
A table header is part of the table, and normally body and header scroll together when the table was restricted to a certain height (mind that you won't see a scrollbar without an explicit height!). Some statements are necessary to make the body scroll independently beneath the header.
The Solutions
I focus on relevant CSS statements only. Nevertheless some colors and borders make it easy to see where the different containers are that make up the solution. A comment will point it out when a CSS statement is just decorative.
1: HTML + CSS "Pinboard" Solution
This solution is built by wrapping the <table>
into two <div>
elements.
The outer one is a pinboard for the header.
The inner one is a scrollpane for the table body.
Pinning is done by CSS position: absolute
.
Mind that for this solution all header cells need to get wrapped into <div>
elements!
First Column |
Second Column |
---|---|
Cell 1.1 | Cell 1.2 |
Cell 2.1 | Cell 2.2 |
Cell 3.1 | Cell 3.2 |
Cell 4.1 | Cell 4.2 |
Cell 5.1 | Cell 4.2 |
Cell 4.1 | Cell 4.2 |
Cell 5.1 | Cell 5.2 |
Cell 6.1 | Cell 6.2 |
Cell 7.1 | Cell 7.2 |
Cell 8.1 | Cell 8.2 |
Advantages:
- You can set any width onto the outer
div
, even percentages - No JavaScript needed, no shivering due to scroll event handling
Disadvantages:
- Can not center the header labels
- No separator line between the header labels
- Need to wrap all header cells into
<div>
elements - Need to wrap the table into two
<div>
elements
CSS:
.headerPinboard /* where the fixed header will be */ { position: relative; /* non-static position, to be parent for absolute */ padding-top: 2em; /* place for the fixed header, may vary */ width: 100%; } .bodyScrollpane { height: 8em; /* table height: without height no scrollbar ever! */ overflow: auto; /* show scrollbar when needed */ border-top: 1px solid black; /* separator line between header and table */ resize: vertical; } .bodyScrollpane table { width: 100%; /* else right side empty space */ } .bodyScrollpane th > div /* the table header cells */ { position: absolute; /* pinned to next non-static parent */ top: 0.4em; /* be at top of parent */ padding-left: 0.4em; } .bodyScrollpane th { padding: 0; /* else compressed empty row on top of table */ }
HTML: You need to wrap all header cells into <div>
elements,
because a <th>
element can not be separated from its table.
The use of <th>
is required, <thead>
is optional.
<div class="headerPinboard"> <div class="bodyScrollpane"> <table> <thead> <tr> <th><div>First Column</div></th> <th><div>Second Column</div></th> </tr> </thead> <tbody> <tr> <td>....</td> <td>....</td> </tr> </tbody> </table> </div> <!-- END bodyScrollpane --> </div> <!-- END headerPinboard -->
2: Pure CSS "Block" Solution
This doesn't use any wrapping element, but it needs fixed column widths, i.e. you must give each cell a fixed width, also the table itself needs a width. The table's width and the cells' widths must play together, else the head will not be aligned to columns. This can be found just by "try and error" (and may also be browser-specific).
First Column | Second Column |
---|---|
Cell 1.1 | Cell 1.2 |
Cell 2.1 | Cell 2.2 |
Cell 3.1 | Cell 3.2 |
Cell 4.1 | Cell 4.2 |
Cell 5.1 | Cell 4.2 |
Cell 4.1 | Cell 4.2 |
Cell 5.1 | Cell 5.2 |
Cell 6.1 | Cell 6.2 |
Cell 7.1 | Cell 7.2 |
Cell 8.1 | Cell 8.2 |
Advantages:
- No additional wrapping DIV elements are needed
- Header labels are centered, vertical separator line is present
- No JavaScript, no shivering
Disadvantages:
- Neither full width nor natural width is achievable (which is a big minus!)
- Width of table and columns must be fixed, percentages destroy the layout
- Column widths must play together with table width, else header not aligned to columns
CSS:
.fixedHeaderCssTable thead { display: block; } .fixedHeaderCssTable tbody { display: block; overflow: auto; height: 8em; resize: vertical; } /* overall width of table, sum of cell widths plus scrollbar */ .fixedHeaderCssTable { width: 50.2em; } /* width for column 1 */ .fixedHeaderCssTable th:nth-child(1), .fixedHeaderCssTable td:nth-child(1) { width: 17em; /* table width depends on this */ } /* width for column 2 */ .fixedHeaderCssTable th:nth-child(2), .fixedHeaderCssTable td:nth-child(2) { width: 30em; /* table width depends on this */ }
HTML:
<table class="fixedHeaderCssTable"> <thead> <tr> <th><div>First Column</div></th> <th><div>Second Column</div></th> </tr> </thead> <tbody> <tr> <td>....</td> <td>....</td> </tr> </tbody> </table>
3: JS Solution
This "transforms" the header on every scroll event to always be on top.
First Column | Second Column |
---|---|
Cell 1.1 | Cell 1.2 |
Cell 2.1 | Cell 2.2 |
Cell 3.1 | Cell 3.2 |
Cell 4.1 | Cell 4.2 |
Cell 5.1 | Cell 4.2 |
Cell 4.1 | Cell 4.2 |
Cell 5.1 | Cell 5.2 |
Cell 6.1 | Cell 6.2 |
Cell 7.1 | Cell 7.2 |
Cell 8.1 | Cell 8.2 |
Advantages:
- Both full width and natural width are achievable
- Header labels are centered
Disadvantages:
- Header shivers due to scroll event handling (big minus!)
- Without CSS
border-collapse: separate;
workaround, header borders are missing generally, especially the vertical separator line between header cells - Border management is generally difficult when table has an outer border
- Browser warning about unstable technique:
This site appears to use a scroll-linked positioning effect. This may not work well with asynchronous panning; see https://developer.mozilla.org/docs/Mozilla/Performance/ScrollLinkedEffects for further details and to join the discussion on related tools and features!
CSS: the border-collapse: separate;
and
border-spacing: 0;
statements add the missing header border.
The elegant table's border-collapse: collapse;
must be given up for that.
But border management generally leaks here when the table has a border,
because then you can see the tabĺe body above the header when you scroll!
.fixedHeaderJsTable { overflow-y: auto; height: 10.5em; /* without height no scrollbar */ resize: vertical; } .fixedHeaderJsTable table { border-collapse: separate; border-spacing: 0; width: 100%; }
HTML:
<div class="fixedHeaderJsTable"> <table> <thead> <tr> <th><div>First Column</div></th> <th><div>Second Column</div></th> </tr> </thead> <tbody> <tr> <td>....</td> <td>....</td> </tr> </tbody> </table> </div>
JS:
var tables = document.querySelectorAll(".fixedHeaderJsTable"); for (var i = 0; i < tables.length; i++) { tables[i].addEventListener("scroll", function() { this.querySelector("thead").style.transform = "translate(0, "+this.scrollTop+"px)"; }); }
Decorative Styling Part
For completeness, here is the purely decorative CSS of the example tables above.
table { border: 3px solid blue; border-collapse: collapse; color: gray; } table tbody { background: lightBlue; } table thead { background: lightGreen; } table td, table th { border: 1px solid black; padding: 0.5em; } /* "Pinboard" solution only: */ .headerPinboard { border: 5px solid green; background: lightSalmon; }
Resume
Personally I prefer solution 1 (pinboard). It is the best compromise.
From stackoverflow forum:
This frozen-headers-for-a-table issue has been an open wound in HTML/CSS for a long time.
That's exactly what I feel. Each of the presented solutions has its weaknesses. Until browsers implement fixed headers we will see just workarounds.