HTML Table with Fixed Header Revisited


Published: 2018-02-15
Updated: 2018-02-22
Web: https://fritzthecat-blog.blogspot.com/2018/02/html-table-with-fixed-header-revisited.html


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:

Disadvantages:

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:

Disadvantages:

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:

Disadvantages:

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.





ɔ⃝ Fritz Ritzberger, 2018-02-15