The headers with position:sticky
want to stick to top of their parent (the table) and not the window because the table has overflow-x:scroll
. I came up with a simple workaround that doesn't rely on a fixed height or a custom scrollbar. It only uses javascript.
The trick was to split the table into two tables. A headers table (tableHeaders
) and a body table (tableBody
). The tableHeaders
has overflow-x:hidden
and the tableBody
has overflow-x:scroll
. Now the headers are no longer inside the table and the body can be scrolled. The only problem is the header does not scroll but we can easily solve this by matching it to the tableBody
scroll using onscroll
event .
Wrap both tables in a div to get the sticky effect to end with the table body.
The last thing we need to do is make sure our column widths in our tableHeaders
match those that were generated in the headers of our tableBody
. We can handle that with a little bit of javascript. Call it somewhere after the content has loaded.
// The FullHeightTable component is expected to build a Sticky Table containing only an exact copy of the Primary Tables headers.
// The Sticky Table should be placed above the Primary Table.
function FixFullHeightTable() {
// Select Primary Table body with built in headers.
var tBody = document.getElementById('tableBody');
var thead = tBody.querySelector('thead');
// Select Sticky Table with only headers. This table contains only a copy of the Primary Table headers with no body.
// It is placed directly above the Primary Table and is intended to replace the Primary Table built in headers.
// It's purpose is to stick to the top of the screen outside of the table markup.
var stickyThead = document.getElementById('tableHeaders').querySelector('thead');
var stickycolumns = stickyThead.querySelectorAll('th');
// Copy the column widths from our hidden Primary table header to our Sticky Table header.
var ths = thead.querySelectorAll('th');
for (var i = 0; i < ths.length; i++) {
var th = ths[i];
// Since the Sticky Table header is expected to be an exact copy of the Primary Table, we know their indicies will be the same.
stickycolumns[i].style.minWidth = th.offsetWidth + 'px';
stickycolumns[i].style.maxWidth = th.offsetWidth + 'px';
}
// Sometimes setting line-height and opacity won't be enough to collapse the header to 0px.
// We'll cover those edge cases by moving the table body over the header.
// Warning: We can't remove Primary table header because they determine some of our column formatting, especially when css is involved.
tBody.style.marginTop = `-${thead.offsetHeight}px`;
}
NOTE: I'm in a Blazor environment and this all gets wrapped into a component where the <thead>
is passed as a RenderFragment
and automatically duplicated. If it were any other scenario I would probably just use javascript to clone the header into a sticky table instead of cluttering the markup.
WARNING: If there are other sticky or fixed elements on the page top should be equal to their heights. I have another example that shows how to automatically handle this.
<div>
<div id="tableHeaders" style="overflow-x:hidden;position:sticky;top:0; background: red;color: white;">
<table >
<thead>
<tr>
<th style="white-space: nowrap !important">1. A Very Long Header That Never Ends</th>
<th style="white-space: nowrap !important">2. A Very Long Header That Never Ends</th>
<th style="white-space: nowrap !important">3. A Very Long Header That Never Ends</th>
<th style="white-space: nowrap !important">4. A Very Long Header That Never Ends</th>
<th style="white-space: nowrap !important">5. A Very Long Header That Never Ends</th>
<th style="white-space: nowrap !important">6. A Very Long Header That Never Ends</th>
</tr>
</thead>
</table>
</div>
<div id="tableBody" style="overflow-x:scroll" onscroll="document.getElementById('tableHeaders').scrollLeft = this.scrollLeft">
<table>
<thead style="line-height:0px;opacity:0">
<tr>
<th style="white-space: nowrap !important">1. A Very Long Header That Never Ends</th>
<th style="white-space: nowrap !important">2. A Very Long Header That Never Ends</th>
<th style="white-space: nowrap !important">3. A Very Long Header That Never Ends</th>
<th style="white-space: nowrap !important">4. A Very Long Header That Never Ends</th>
<th style="white-space: nowrap !important">5. A Very Long Header That Never Ends</th>
<th style="white-space: nowrap !important">6. A Very Long Header That Never Ends</th>
</tr>
</thead>
<tbody>
<tr>
<td style="white-space: nowrap !important" >1. This is some very long content</td>
<td style="white-space: nowrap !important" >2. This is some very long content</td>
<td style="white-space: nowrap !important" >3. This is some very long content</td>
<td style="white-space: nowrap !important" >4. This is some very long content</td>
<td style="white-space: nowrap !important" >5. This is some very long content</td>
<td style="white-space: nowrap !important" >6. This is some very long content</td>
</tr>
<tr>
<td style="white-space: nowrap !important" >1. This is some very long content</td>
<td style="white-space: nowrap !important" >2. This is some very long content</td>
<td style="white-space: nowrap !important" >3. This is some very long content</td>
<td style="white-space: nowrap !important" >4. This is some very long content</td>
<td style="white-space: nowrap !important" >5. This is some very long content</td>
<td style="white-space: nowrap !important" >6. This is some very long content</td>
</tr>
<tr>
<td style="white-space: nowrap !important" >1. This is some very long content</td>
<td style="white-space: nowrap !important" >2. This is some very long content</td>
<td style="white-space: nowrap !important" >3. This is some very long content</td>
<td style="white-space: nowrap !important" >4. This is some very long content</td>
<td style="white-space: nowrap !important" >5. This is some very long content</td>
<td style="white-space: nowrap !important" >6. This is some very long content</td>
</tr>
<tr>
<td style="white-space: nowrap !important" >1. This is some very long content</td>
<td style="white-space: nowrap !important" >2. This is some very long content</td>
<td style="white-space: nowrap !important" >3. This is some very long content</td>
<td style="white-space: nowrap !important" >4. This is some very long content</td>
<td style="white-space: nowrap !important" >5. This is some very long content</td>
<td style="white-space: nowrap !important" >6. This is some very long content</td>
</tr>
<tr>
<td style="white-space: nowrap !important" >1. This is some very long content</td>
<td style="white-space: nowrap !important" >2. This is some very long content</td>
<td style="white-space: nowrap !important" >3. This is some very long content</td>
<td style="white-space: nowrap !important" >4. This is some very long content</td>
<td style="white-space: nowrap !important" >5. This is some very long content</td>
<td style="white-space: nowrap !important" >6. This is some very long content</td>
</tr>
<tr>
<td style="white-space: nowrap !important" >1. This is some very long content</td>
<td style="white-space: nowrap !important" >2. This is some very long content</td>
<td style="white-space: nowrap !important" >3. This is some very long content</td>
<td style="white-space: nowrap !important" >4. This is some very long content</td>
<td style="white-space: nowrap !important" >5. This is some very long content</td>
<td style="white-space: nowrap !important" >6. This is some very long content</td>
</tr>
</tbody>
</table>
</div>
</div>
<div style="min-height:2000px">
<p> Just some long body content</p>
</div>
BONUS: Sticky columns are even easier! Just apply the following styles to the <td>
element;
position: sticky;
left: 0px;