table with sticky header and horizontal scroll
Asked Answered
V

2

13

I want a table with sticky header and horizontal scroll.

I dont want a vertical scroll inside the table. It should scroll with the page. So no set height of the table. Is this even possible?

Here is codepen of not working example: https://codepen.io/fwitkowski/pen/zYEQvvg

When I remove overflow: auto from table-container, position sticky works just fine.

.table-container {
  max-width:350px;
  overflow-x:auto; /* for horizontal scroll */
  position: relative; /* relative to the normal flow */
  border: solid 5px red /* for reference */
}
Vite answered 19/1, 2022 at 11:44 Comment(0)
A
18

As per the MDN documentation:

a sticky element "sticks" to its nearest ancestor that has a "scrolling mechanism" (created when overflow is hidden, scroll, auto, or overlay), even if that ancestor isn't the nearest actually scrolling ancestor.

There's an active GitHub issue discussing this on the W3C repo, which has been running since 2017. There have been various workarounds suggested, but they all seem to rely on adding a fixed height to the table / table container, or using Javascript as in this answer.

At least for the moment, this is not something that's supported natively.

Ansilma answered 19/1, 2022 at 14:48 Comment(3)
I think this is right (unfortunatelly) I will go with max height and have scrollbars for both table container (X and Y)Vite
Is there a JS solution that can get around this? Seems crazy if not!Readable
@Readable I linked to one in my answer. :)Ansilma
A
6

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;
Atwell answered 23/10, 2023 at 18:6 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.