184

I would like to be able to "tweak" an HTML table's presentation to add a single feature: when scrolling down through the page so that the table is on the screen but the header rows are off-screen, I would like the headers to remain visible at the top of the viewing area.

This would be conceptually like the "freeze panes" feature in Excel. However, an HTML page might contain several tables in it and I only would want it to happen for the table that is currently in-view, only while it is in-view.

Note: I've seen one solution where the table data area is made scrollable while the headers do not scroll. That's not the solution I'm looking for.

C R
  • 2,182
  • 5
  • 32
  • 41
Craig McQueen
  • 41,871
  • 30
  • 130
  • 181
  • is your page designed using tables? is it not possible to convert it to divs first and leave tabular data alone for the tables? – lock Jun 23 '09 at 00:19
  • 7
    i recently wrote a plugin that does this: http://programmingdrunk.com/floatThead/ – mkoryak Oct 16 '12 at 19:29
  • 1
    In addition to the top-voted solution and the derivatives of it below, @mkoryak 's plugin above is also quite good. Be sure to take a look at that too before you finish "shopping". – DanO Jan 21 '14 at 17:09
  • Thanks dan :) I have some kick-ass features planned for the next release too – mkoryak Jan 21 '14 at 17:26
  • I second the @mkoryak floatThead plugin - I was able to swiftly integrate it. Big thanks for the plugin! – w5m Jan 12 '17 at 17:33

10 Answers10

70

Check out jQuery.floatThead (demos available) which is very cool, can work with DataTables too, and can even work inside an overflow: auto container.

Hendy Irawan
  • 20,498
  • 11
  • 103
  • 114
  • yes it works with horizontal scrolling, window scrolling, inner scrolling and window resizing. – mkoryak Nov 19 '13 at 02:09
  • Could you add a fiddle with a working code? (I know that there is in the site...) – Michel Ayres Mar 14 '14 at 14:53
  • 12
    i've built a huge demo / docs page. Why do you need a fiddle? – mkoryak Mar 24 '14 at 16:55
  • It works, but column widths are preserved only with jQuery 1, not jQuery 2. Bug? – Eugene Kardash Jul 09 '14 at 19:59
  • Very nice, except it doesn't work with "display: flex" layouts (i.e. if you want to make the table vertically fill a space. :-/ This is because the wrapper div it makes is "position: relative" which doesn't seem to play nice with that. – Alastair Maw Jul 31 '14 at 15:44
  • there is a workaround for the 'display:flex' issue - add 1 css rule (see this discussion: https://github.com/mkoryak/floatThead/pull/45) – mkoryak Aug 27 '14 at 00:23
  • To me, in latest Firefox Dev, the floatThead doesn't even work on it's own demo page: http://mkoryak.github.io/floatThead/ (but congrats on all the hard work, mkoryak) – userfuser May 20 '15 at 11:48
  • @mkoryak: quick feedback: your tool looks amazing. I was looking for "here's your table code, here's your javascript code." but it was all just the javascript. The tables in your examples weren't simple... had all kinds of classes that may or may not have been relevant. Tried to duplicate on my site and it didn't work. If it was a simpler example I might be able to figure out why. So in short, if you could put an MVP sample at the top that would be really fantastic in terms of bootstrapping people. Because I really want to see a simple example work on my site before I invest too much time. – MustModify Jan 04 '16 at 22:14
  • Interesting alternative to StickyTableHeaders. Based on my brief tests, it looks like it has the ability to handle thousands of rows of data without performance degradation above and below the table as long as floatThead is enabled. I'll probably give it a whirl when I have time. – CubicleSoft Jun 12 '16 at 03:38
  • 5
    @MustModify stackoverflow is not a good place to ask me questions. report an issue on github, even if its just a question about how to get it working. Ill respond there usually in under a day. Here ill probably respond in just under a year :) – mkoryak Oct 04 '16 at 18:21
  • 1
    @MustModify for what its worth, none of the classes on the tables in the examples are relevant. The plugin does not need any css or classes to work, as my docs say. – mkoryak Oct 04 '16 at 18:23
  • @mkoryak thanks man. Your tool has helped me achieve a long pending goal in seconds. – Parminder Apr 16 '19 at 08:50
67

Craig, I refined your code a bit (among a few other things it's now using position:fixed) and wrapped it as a jQuery plugin.

Try it out here: http://jsfiddle.net/jmosbech/stFcx/

And get the source here: https://github.com/jmosbech/StickyTableHeaders

jmosbech
  • 1,128
  • 9
  • 8
  • 1
    Good idea to make a jQuery plugin. A couple of questions: (1) Does it work well with horizontal scrolling? (2) How does it behave when you scroll down so the bottom of the table is scrolling off the top of the window? The example doesn't allow testing of those cases. – Craig McQueen Oct 10 '11 at 22:04
  • Your jsfiddle example works great with tablesorter, but the code on github does not. Did you have to modify the tablesorter code? – ericslaw Oct 12 '11 at 18:43
  • Craig, horizontal scrolling shouldn't be a problem since the header is re-positioned both horizontally and vertically on scroll and resize. As for your second question: The header is hidden when the table leaves the viewport. – jmosbech Oct 12 '11 at 20:16
  • Eric, that's strange. Did you try to open /demo/tablesorter.htm from Github? I haven't modified the tablesorter code, but in order to make it work you need to apply the StickyTableHeader plugin before tablesorter. – jmosbech Oct 12 '11 at 20:19
  • Eric & Craig, I've modified the tablesorter demo from github to illustrate what happens on horizontal and vertical scrolling and uploaded it [here](http://173.dk/stickytableheaders/demo/tablesorter.htm) - go ahead and give it a spin and let me know if something doesn't work as expected. – jmosbech Oct 12 '11 at 20:29
  • Your code doesn't work for IE (tested on 6 and 9), while Craig's is ok. I browsed test pages provided by him and you. – pavel_kazlou Nov 23 '11 at 09:13
  • Pavel, thats funny, it works fine in IE9 here. Could you be a little more specific? You're right about IE6 though. That's due to the use of position:fixed. – jmosbech Nov 23 '11 at 12:57
  • Awesome job on the plugin. Works great and nice job throwing in the bootstrap fix and offset option. – gspatel Apr 27 '12 at 17:50
  • 2
    The horizonal scrolling doesn't quite work, after scrolling quite a distance the thead is positioned further left when scrolled off-screen than when it is positioned normally as part of the table. – Nathan Phillips Jul 23 '13 at 16:34
  • Awesome! Thank you Craig, @jmosbech, and Tarek ! :) Works very well for us after integrating with our Wicket library (see https://github.com/soluvas/soluvas-web/commit/99f808b212437f6e89121e3c8b6941e1ee9554a5 ). With a bit of CSS it looks awesome in Bootstrap tables too :) – Hendy Irawan Oct 02 '13 at 16:14
  • @jmosbech this works great, except when I scroll down to where the header is "sticky" at the top, the width of the header row shrinks so it's only about 75% of the width of the actual rows of data. Any suggestions? – ganders Nov 21 '13 at 14:42
  • @ganders, it's probably a border-collapse/border-spacing issue. Check out the samples in the github repo for samples. Otherwise feel free to submit an issue. – jmosbech Nov 21 '13 at 20:25
  • @jmosbech just figured out what it was. It had to do with box-sizing: border-box since I'm using bootstrap 3. I had to modify the line that sets each th width to use the origCell.css('width') instead of origCell.width(). I just checked out your jsFiddle again, did you drastically change it since yesterday? – ganders Nov 22 '13 at 15:57
  • Very nice job. One issue though is it doesn't quite work with `border-collapse`. – Mike Feb 18 '14 at 06:53
  • Does this work for a table not at the top-left of a page as in the demo? – khaverim May 25 '14 at 01:16
  • You saved me a lot of pain and suffering. Thank you so much! – Michael S. Miller Mar 20 '19 at 17:53
34

If you're targeting modern css3 compliant browsers (Browser support: https://caniuse.com/#feat=css-sticky) you can use position:sticky, which doesn't require JS and won't break the table layout miss-aligning th and td of the same column. Nor does it require fixed column width to work properly.

Example for a single header row:

thead th
{
    position: sticky;
    top: 0px;
}

For theads with 1 or 2 rows, you can use something like this:

thead > :last-child th
{
    position: sticky;
    top: 30px; /* This is for all the the "th" elements in the second row, (in this casa is the last child element into the thead) */
}

thead > :first-child th
{
    position: sticky;
    top: 0px; /* This is for all the the "th" elements in the first child row */
}

You might need to play a bit with the top property of the last child changing the number of pixels to match the height of the first row (+ the margin + the border + the padding, if any), so the second row sticks just down bellow the first one.

Also both solutions work even if you have more than one table in the same page: the th element of each one starts to be sticky when its top position is the one indicated into the css definition and just disappear when all the table scrolls down. So if there are more tables all work beautifully the same way.

Why to use last-child before and first-child after in the css?

Because css rules are rendered by the browser in the same order as you write them into the css file and because of this if you have just 1 row into the thead element the first row is simultaneously the last row too and the first-child rule need to override the last-child one. If not you will have an offset of the row 30 px from the top margin which I suppose you don't want to.

A known problem of position: sticky is that it doesn't work on thead elements or table rows: you must target th elements. Hopping this issue will be solved on future browser versions.

willy wonka
  • 1,440
  • 1
  • 18
  • 31
12

The most simple answer only using CSS :D !!!

table {
  /* Not required only for visualizing */
  border-collapse: collapse;
  width: 100%;
}

table thead tr th {
  /* you could also change td instead th depending your html code */
  background-color: green;
  position: sticky;
  z-index: 100;
  top: 0;
}

td {
  /* Not required only for visualizing */
  padding: 1em;
}
<table>
  <thead>
    <tr>
      <th>Col1</th>
      <th>Col2</th>
      <th>Col3</th>
    </tr>
  </thead>
  <tbody>
     <tr>
       <td>data</td>
       <td>data</td>
       <td>data</td>
     </tr>
     <tr>
       <td>data</td>
       <td>data</td>
       <td>data</td>
     </tr>
     <tr>
       <td>data</td>
       <td>data</td>
       <td>data</td>
     </tr>
     <tr>
       <td>data</td>
       <td>data</td>
       <td>data</td>
     </tr>
     <tr>
       <td>data</td>
       <td>data</td>
       <td>data</td>
     </tr>
     <tr>
       <td>data</td>
       <td>data</td>
       <td>data</td>
     </tr>
     <tr>
       <td>data</td>
       <td>data</td>
       <td>data</td>
     </tr>
     <tr>
       <td>data</td>
       <td>data</td>
       <td>data</td>
     </tr>
     <tr>
       <td>data</td>
       <td>data</td>
       <td>data</td>
     </tr>
     <tr>
       <td>data</td>
       <td>data</td>
       <td>data</td>
     </tr>
     <tr>
       <td>data</td>
       <td>data</td>
       <td>data</td>
     </tr>
     <tr>
       <td>data</td>
       <td>data</td>
       <td>data</td>
     </tr>
     <tr>
       <td>data</td>
       <td>data</td>
       <td>data</td>
     </tr>
     <tr>
       <td>data</td>
       <td>data</td>
       <td>data</td>
     </tr>
     <tr>
       <td>david</td>
       <td>castro</td>
       <td>rocks!</td>
     </tr>
  </tbody>
</table>
David Castro
  • 1,773
  • 21
  • 21
5

I've encountered this problem very recently. Unfortunately, I had to do 2 tables, one for the header and one for the body. It's probably not the best approach ever but here goes:

<html>
<head>
    <title>oh hai</title>
</head>
<body>
    <table id="tableHeader">
        <tr>
            <th style="width:100px; background-color:#CCCCCC">col header</th>
            <th style="width:100px; background-color:#CCCCCC">col header</th>
        </tr>
    </table>
    <div style="height:50px; overflow:auto; width:250px">
        <table>
            <tr>
                <td style="height:50px; width:100px; background-color:#DDDDDD">data1</td>
                <td style="height:50px; width:100px; background-color:#DDDDDD">data1</td>
            </tr>
            <tr>
                <td style="height:50px; width:100px; background-color:#DDDDDD">data2</td>
                <td style="height:50px; width:100px; background-color:#DDDDDD">data2</td>
            </tr>
        </table>
    </div>
</body>
</html>

This worked for me, it's probably not the elegant way but it does work. I'll investigate so see if I can do something better, but it allows for multiple tables.

Go read on the overflow propriety to see if it fits your need

Sergej
  • 1,082
  • 11
  • 27
Gab Royer
  • 9,587
  • 8
  • 40
  • 58
  • Hmm not working, i'll try to fix this, but the approach remains, you have to use the overflow:auto proprety – Gab Royer Jun 23 '09 at 01:41
  • I appreciate your solution and its simplicity, however in my question I said "Note: I've seen one solution where the table data area is made scrollable while the headers do not scroll. That's not the solution I'm looking for." – Craig McQueen Jun 23 '09 at 04:32
  • @GabRoyer The w3school page, you referenced, does not exist. – Farhan Sep 04 '13 at 20:15
  • They seem to have moved it. Fixed – Gab Royer Sep 05 '13 at 19:33
  • Another problem with this approach is that if you need to adapt some of the columns to other content you risk to break their alignment with the corresponding column header. IMHO it is better to face this problem with an approach that allows a kinda of dynamic management of the content that keeps a perfect alignment of columns and relative heads. So IMHO **position=sticky** is the way to go. – willy wonka Feb 04 '19 at 10:17
5

Possible alternatives

js-floating-table-headers

js-floating-table-headers (Google Code)

In Drupal

I have a Drupal 6 site. I was on the admin "modules" page, and noticed the tables had this exact feature!

Looking at the code, it seems to be implemented by a file called tableheader.js. It applies the feature on all tables with the class sticky-enabled.

For a Drupal site, I'd like to be able to make use of that tableheader.js module as-is for user content. tableheader.js doesn't seem to be present on user content pages in Drupal. I posted a forum message to ask how to modify the Drupal theme so it's available. According to a response, tableheader.js can be added to a Drupal theme using drupal_add_js() in the theme's template.php as follows:

drupal_add_js('misc/tableheader.js', 'core');
Craig McQueen
  • 41,871
  • 30
  • 130
  • 181
3

If you use a full screen table you are maybe interested in setting th to display:fixed; and top:0; or try a very similar approach via css.

Update

Just quickly build up a working solution with iframes (html4.0). This example IS NOT standard conform, however you will easily be able to fix it:

outer.html

<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">   
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">     
    <head>      
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />   
        <title>Outer</title>
  <body>
    <iframe src="test.html" width="200" height="100"></iframe>
    </body>
</html> 

test.html

<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">   
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">     
    <head>      
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />   
        <title>Floating</title>
    <style type="text/css">
      .content{
        position:relative; 
      }

      thead{
        background-color:red;
        position:fixed; 
        top:0;
      }
    </style>
  <body>
    <div class="content">      
      <table>
        <thead>
          <tr class="top"><td>Title</td></tr>
        </head>
        <tbody>
          <tr><td>a</td></tr>
          <tr><td>b</td></tr>
          <tr><td>c</td></tr>
          <tr><td>d</td></tr>
          <tr><td>e</td></tr>
          <tr><td>e</td></tr>
          <tr><td>e</td></tr>
          <tr><td>e</td></tr>
          <tr><td>e</td></tr>
          <tr><td>e</td></tr>
        </tbody>
      </table>
    </div>
    </body>
</html> 
merkuro
  • 6,161
  • 2
  • 27
  • 29
  • @a_m0d, true...but the request sounds a big confusing. Either we are missunderstanding it, or the asker doesn't completely understand what he wants. – Sampson Jun 23 '09 at 00:41
  • Here is another solution using some javascript http://www.imaputz.com/cssStuff/bigFourVersion.html – merkuro Jun 23 '09 at 01:02
3

Using display: fixed on the thead section should work, but for it only work on the current table in view, you will need the help of JavaScript. And it will be tricky because it will need to figure out scrolling places and location of elements relative to the viewport, which is one of the prime areas of browser incompatibility.

Have a look at the popular JavaScript frameworks (jQuery, MooTools, YUI, etc etc.) to see if they can either do what you want or make it easier to do what you want.

staticsan
  • 29,935
  • 4
  • 60
  • 73
3

This is really a tricky thing to have a sticky header on your table. I had same requirement but with asp:GridView and then I found it really thought to have sticky header on gridview. There are many solutions available and it took me 3 days trying all the solution but none of them could satisfy.

The main issue that I faced with most of these solutions was the alignment problem. When you try to make the header floating, somehow the alignment of header cells and body cells get off track.

With some solutions, I also got issue of getting header overlapped to first few rows of body, which cause body rows getting hidden behind the floating header.

So now I had to implement my own logic to achieve this, though I also not consider this as perfect solution but this could also be helpful for someone,

Below is the sample table.

<div class="table-holder">
        <table id="MyTable" cellpadding="4" cellspacing="0" border="1px" class="customerTable">
            <thead>
                <tr><th>ID</th><th>First Name</th><th>Last Name</th><th>DOB</th><th>Place</th></tr>
            </thead>
            <tbody>
                <tr><td>1</td><td>Customer1</td><td>LastName</td><td>1-1-1</td><td>SUN</td></tr>
                <tr><td>2</td><td>Customer2</td><td>LastName</td><td>2-2-2</td><td>Earth</td></tr>
                <tr><td>3</td><td>Customer3</td><td>LastName</td><td>3-3-3</td><td>Mars</td></tr>
                <tr><td>4</td><td>Customer4</td><td>LastName</td><td>4-4-4</td><td>Venus</td></tr>
                <tr><td>5</td><td>Customer5</td><td>LastName</td><td>5-5-5</td><td>Saturn</td></tr>
                <tr><td>6</td><td>Customer6</td><td>LastName</td><td>6-6-6</td><td>Jupitor</td></tr>
                <tr><td>7</td><td>Customer7</td><td>LastName</td><td>7-7-7</td><td>Mercury</td></tr>
                <tr><td>8</td><td>Customer8</td><td>LastName</td><td>8-8-8</td><td>Moon</td></tr>
                <tr><td>9</td><td>Customer9</td><td>LastName</td><td>9-9-9</td><td>Uranus</td></tr>
                <tr><td>10</td><td>Customer10</td><td>LastName</td><td>10-10-10</td><td>Neptune</td></tr>
            </tbody>
        </table>
    </div>

Note: The table is wrapped into a DIV with class attribute equal to 'table-holder'.

Below is the JQuery script that I added in my html page header.

<script src="../Scripts/jquery-1.7.2.min.js" type="text/javascript"></script>
<script src="../Scripts/jquery-ui.min.js" type="text/javascript"></script>
<script type="text/javascript">
    $(document).ready(function () {
        //create var for table holder
        var originalTableHolder = $(".table-holder");
        // set the table holder's with
        originalTableHolder.width($('table', originalTableHolder).width() + 17);
        // Create a clone of table holder DIV
        var clonedtableHolder = originalTableHolder.clone();

        // Calculate height of all header rows.
        var headerHeight = 0;
        $('thead', originalTableHolder).each(function (index, element) {
            headerHeight = headerHeight + $(element).height();
        });

        // Set the position of cloned table so that cloned table overlapped the original
        clonedtableHolder.css('position', 'relative');
        clonedtableHolder.css('top', headerHeight + 'px');

        // Set the height of cloned header equal to header height only so that body is not visible of cloned header
        clonedtableHolder.height(headerHeight);
        clonedtableHolder.css('overflow', 'hidden');

        // reset the ID attribute of each element in cloned table
        $('*', clonedtableHolder).each(function (index, element) {
            if ($(element).attr('id')) {
                $(element).attr('id', $(element).attr('id') + '_Cloned');
            }
        });

        originalTableHolder.css('border-bottom', '1px solid #aaa');

        // Place the cloned table holder before original one
        originalTableHolder.before(clonedtableHolder);
    });
</script>

and at last below is the CSS class for bit of coloring purpose.

.table-holder
{
    height:200px;
    overflow:auto;
    border-width:0px;    
}

.customerTable thead
{
    background: #4b6c9e;        
    color:White;
}

So the whole idea of this logic is to place the table into a table holder div and create clone of that holder at client side when page loaded. Now hide the body of table inside clone holder and position the remaining header part over to original header.

Same solution also works for asp:gridview, you need to add two more steps to achieve this in gridview,

  1. In OnPrerender event of gridview object in your web page, set the table section of header row equal to TableHeader.

    if (this.HeaderRow != null)
    {
        this.HeaderRow.TableSection = TableRowSection.TableHeader;
    }
    
  2. And wrap your grid into <div class="table-holder"></div>.

Note: if your header has clickable controls then you may need to add some more jQuery script to pass the events raised in cloned header to original header. This code is already available in jQuery sticky-header plugin create by jmosbech

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
pgcan
  • 1,199
  • 14
  • 24
  • +1 for good effort, but for large tables with a lot of data, it is pretty bad to clone the whole thing... I imagine that would double loading time – khaverim May 24 '14 at 23:50
0

It's frustrating that what works great in one browser doesn't work in others. The following works in Firefox, but not in Chrome or IE:

<table width="80%">

 <thead>

 <tr>
  <th>Column 1</th>
  <th>Column 2</th>
  <th>Column 3</th>
 </tr>

 </thead>

 <tbody style="height:50px; overflow:auto">

  <tr>
    <td>Cell A1</td>
    <td>Cell B1</td>
    <td>Cell C1</td>
  </tr>

  <tr>
    <td>Cell A2</td>
    <td>Cell B2</td>
    <td>Cell C2</td>
  </tr>

  <tr>
    <td>Cell A3</td>
    <td>Cell B3</td>
    <td>Cell C3</td>
  </tr>

 </tbody>

</table>
Peter Dow
  • 9
  • 1
  • 4
    I'm trying it in Firefox 8.0, and it doesn't seem to do anything. What does it do? – Craig McQueen Dec 15 '11 at 01:38
  • What is `px`? Is it that legacy unit used by people on the 90th when a display device was used to have 72 dpi? – ceving Oct 11 '16 at 16:57
  • I agree: It is very frustrating, Unfortunately browser developers don't respect standards in every situation... and some years ago was even worse! Now some issues have been solved and more browsers behaves in a more standardized way, but a lot of steps still have to be done yet: let's hope and pray :-) – willy wonka Sep 08 '18 at 09:58