Javascript – Scrolling a table with multiple fixed table headers

html-tablejavascriptjqueryscroll

Before I attempt to reinvent the wheel (via jQuery plugin or similar), I'm trying to see if there is an easier way to do this or an existing plugin that users may know of. What I'm looking to do is scroll the body of a table that contains multiple table headers. For example, imagine something of this structure:

<table>
    <thead>
        <tr>
            <td colspan="2"></td>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td></td>
            <td></td>
        </tr>
    </tbody>
    <thead>
        <tr>
            <td colspan="2"></td>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td></td>
            <td></td>
        </tr>
        <tr>
           <td></td>
           <td></td>
        </tr>
        <tr>
            <td></td>
            <td></td>
        </tr>
    </tbody>
</table>

Honestly, I haven't tried out the above syntax to even see if its valid. The actual markup I have does not currently use thead/tbody and instead scrolls the whole parent div (seen below).

example table with multiple headers

What I want to achieve is scrolling of the whole table such that the header of the most relevant section is viewed on top. Currently the header scrolls out of view is there are enough rows.

I know the various techniques used to scroll a table with one header, but what about multiple? Are there any existing ways to achieve this? I'm open to different ideas, but right now I'm thinking of simply displaying the most relevant table header for the content on top.

Best Answer

A few months ago, I wrote some code that did exactly that where I wanted the headers to perform similar to section headers on iOS. Utilising a Jquery, the solution I ended up with involved creating an onScroll (and onResize for window resizing) event listener that ran a check through all $('table thead') and checked their $(this).position() on the page.

The check was whether the thead had a position which was above the top of the current viewport.

Once I had found the most relevant header (the bottommost thead that was above the viewport), I created a new table, with position: fixed at (0, 0) and copied into it a new column for each column of the headers rows and manually set its width properties to match the original table.

I have put together a Proof of Concept which shows how this all works.

Here is some pseudo-code of how it works:

  1. Check the table is visible
  2. Get a list of all thead sections in the table
  3. Reverse the list
  4. Find the first thead in the array with a top position less than scrollTop of the body element
  5. If we found one:
    1. Create deep copy of the thead
    2. Make a container table
    3. Copy attributes from the original table so the styles get copied
    4. Position the container at [0, originalTable.left]
    5. Set the width equal to the outerWidth of the original table
    6. Set the width of every td found to the width of the matching td from the original table
    7. Add it to the DOM
  6. If none found, remove the existing container from the DOM if there was one

There are some other edge case details that made this even nicer too:

  • Instead of (0, 0), the origin point for the fixed row was altered based upon whether the "real" header row for the tbody below the one with the fixed header should be pushing it away.
  • Ensure that previous header rows are deleted before making a new one
  • Don't recreate the header row if the header you'd be making is the same as what is already there

This approach worked a lot better others I tried such as trying to position:absolute an object due to the fact that Firefox and IE aren't incredibly fast at running the onScroll handler so you tend to see 'juddering'. I also tried mucking with the position attribute of the theads, this just ended in the table column widths jumping about and not matching the data.

thead nodes aren't strictly required for this solution as you can use some other selector to determine which rows are headers etc.


Update: Added example code and pseudo-code Update: Dropbox changed their system, replaced example with jsfiddle URL