32

I've been searching over the web for the way to make a table with fixed (frozen) columns and header. Seems like I finally found the solution and modified it to my needs.

There original fiddle is here.

Here is my modified solution. I tested it in Chrome (version: 55.0.2883.87 m) and Firefox (version: 51.0.1).

The problem is that it works not completely in IE (version: 11.0.9600.18427). During the horizontal scrolling a frozen part of the header is getting scrolled too. Could someone help me to make it working in IE? And one more question: is the approach safe to use? I mean if it's using some unspecified behavior, then some of the future browsers or even some of the modern browsers might display my table in a wrong way, and it's better to use a safe solution with a few different tables and synchronizing scroll position and rows height. UPD: one more question: how to make this work stable on the mobile devices?

Here is some code that demonstrates the approach:

$(document).ready(function() {
  $('tbody').scroll(function(e) { //detect a scroll event on the tbody
   /*
    Setting the thead left value to the negative valule of tbody.scrollLeft will make it track the movement
    of the tbody element. Setting an elements left value to that of the tbody.scrollLeft left makes it maintain    it's relative position at the left of the table.    
    */
    $('thead').css("left", -$("tbody").scrollLeft()); //fix the thead relative to the body scrolling
    $('thead th:nth-child(1)').css("left", $("tbody").scrollLeft()); //fix the first cell of the header
    $('tbody td:nth-child(1)').css("left", $("tbody").scrollLeft()); //fix the first column of tdbody
  });
});
.container {
  height:200px; 
  width:400px;
  overflow: hidden;
}

table {
  position: relative;
  background-color: #aaa;
  border-collapse: collapse;
table-layout: fixed;
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
}


/*thead*/
thead {
  position: relative;
  display: block; /*seperates the header from the body allowing it to be positioned*/
}

thead th {
  background-color: #99a;
  min-width: 120px;
  border: 1px solid #222;
}

thead th:nth-child(1) {/*first cell in the header*/
  position: relative;
  background-color: #88b;
}


/*tbody*/
tbody {
  flex: 1;
  position: relative;
  display: block; /*seperates the tbody from the header*/
  overflow: auto;
}

tbody td {
  background-color: #bbc;
  min-width: 120px;
  border: 1px solid #222;
}

tbody tr td:nth-child(1) {  /*the first cell in each tr*/
  position: relative;
  background-color: #99a;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<body>
<div class="container">

  <table>
    <thead>
      <tr>
        <th>Name<br/>123</th>
        <th>Town</th>
        <th>County</th>
        <th>Age</th>
        <th>Profession</th>
        <th>Anual Income</th>
        <th>Matital Status</th>
        <th>Children</th>
      </tr>
       <tr>
        <th>Name</th>
        <th>Town</th>
        <th>County</th>
        <th>Age<br/>123<br/>321</th>
        <th>Profession</th>
        <th>Anual Income</th>
        <th>Matital Status</th>
        <th>Children</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>John Smith</td>
        <td>Macelsfield</td>
        <td>Cheshire<br/>123</td>
        <td>52</td>
        <td>Brewer</td>
        <td>£47,000</td>
        <td>Married</td>
        <td>2</td>
      </tr>
      <tr>
        <td>Jenny Jones<br/>123<br/>312</td>
        <td>Threlkeld</td>
        <td>Cumbria</td>
        <td>34</td>
        <td>Shepherdess</td>
        <td>£28,000</td>
        <td>Single</td>
        <td>0</td>
      </tr>
      <tr>
        <td>Peter Frampton</td>
        <td>Avebury</td>
        <td>Wiltshire</td>
        <td>57</td>
        <td>Musician</td>
        <td>£124,000</td>
        <td>Married</td>
        <td>4</td>
      </tr>
      <tr>
        <td>Simon King</td>
        <td>Malvern</td>
        <td>Worchestershire</td>
        <td>48</td>
        <td>Naturalist</td>
        <td>£65,000</td>
        <td>Married</td>
        <td>2</td>
      </tr>
      <tr>
        <td>Lucy Diamond</td>
        <td>St Albans</td>
        <td>Hertfordshire</td>
        <td>67</td>
        <td>Pharmasist</td>
        <td>Retired</td>
        <td>Married</td>
        <td>3</td>
      </tr>
      <tr>
        <td>Austin Stevenson</td>
        <td>Edinburgh</td>
        <td>Lothian </td>
        <td>36</td>
        <td>Vigilante</td>
        <td>£86,000</td>
        <td>Single</td>
        <td>Unknown</td>
      </tr>
      <tr>
        <td>Wilma Rubble</td>
        <td>Bedford</td>
        <td>Bedfordshire</td>
        <td>43</td>
        <td>Housewife</td>
        <td>N/A</td>
        <td>Married</td>
        <td>1</td>
      </tr>
      <tr>
        <td>Kat Dibble</td>
        <td>Manhattan</td>
        <td>New York</td>
        <td>55</td>
        <td>Policewoman</td>
        <td>$36,000</td>
        <td>Single</td>
        <td>1</td>
      </tr>
      <tr>
        <td>Henry Bolingbroke</td>
        <td>Bolingbroke</td>
        <td>Lincolnshire</td>
        <td>45</td>
        <td>Landowner</td>
        <td>Lots</td>
        <td>Married</td>
        <td>6</td>
      </tr>
      <tr>
        <td>Alan Brisingamen</td>
        <td>Alderley</td>
        <td>Cheshire</td>
        <td>352</td>
        <td>Arcanist</td>
        <td>A pile of gems</td>
        <td>Single</td>
        <td>0</td>
      </tr>
    </tbody>
  </table>
  
</div>
</body>
Artem Kachanovskyi
  • 1,839
  • 2
  • 18
  • 27
  • try this thead th:nth-child(1) {/*first cell in the header*/ position: relative; background-color: #88b; display: block; } – Shailesh Singh Feb 02 '17 at 15:09
  • @Singh87, it helps for the cell the stay on the place during scrolling, but the cells loses the ability to automatically adjust it's height. – Artem Kachanovskyi Feb 02 '17 at 15:18
  • I'd suggest considering DataTables - https://datatables.net/extensions/fixedcolumns/examples/initialisation/left_right_columns.html. It supports freezing rows. – K Scandrett Feb 22 '17 at 08:17
  • With this question and answers i hope you will get help. http://stackoverflow.com/questions/1312236/how-do-i-create-an-html-table-with-fixed-frozen-left-column-and-scrollable-body – pathak tejpal Feb 22 '17 at 11:56

4 Answers4

8

This is very peculiar. It appears that the problematic code is this line:

$('thead').css("left", -$("tbody").scrollLeft()); //fix the thead relative to the body scrolling

It looks like IE11 handles relative positioning of nested elements differently (than Chrome and other browsers). In this case, you are positioning thead with relative positioning with an offset. You are also positioning thead th (it's children) with an offset and relative positioning. Chrome appears to be positioning thead relative to table, and then positioning th relative to thead. IE11, on the other hand, appears to be positioning thead relative to table, and then th just inherits that same positioning regardless of its own positioning.

A fix for this would be the following: on IE11, handle the positioning differently for thead. Instead of setting the positioning on the parent thead, set the positioning on the thead th elements. In that way, your first column will not be 'forced' to inherit thead's positioning (in IE).

$(document).ready(function() {
  var isIE11 = !!navigator.userAgent.match(/Trident.*rv\:11\./);
  var customScroller;
  if (isIE11)
    customScroller = function() {
      $('thead th').css("left", -$("tbody").scrollLeft()); //if using IE11, fix the th element 
    };
  else
    customScroller = function() {
      $('thead').css("left", -$("tbody").scrollLeft()); //if not using IE11, fix the thead element
    };

  $('tbody').scroll(function(e) { //detect a scroll event on the tbody
    /*
    Setting the thead left value to the negative valule of tbody.scrollLeft will make it track the movement
    of the tbody element. Setting an elements left value to that of the tbody.scrollLeft left makes it maintain             it's relative position at the left of the table.    
    */
    customScroller(); //fix the thead relative to the body scrolling
    $('thead th:nth-child(1)').css("left", $("tbody").scrollLeft());
//fix the first cell of the header
    $('tbody td:nth-child(1)').css("left", $("tbody").scrollLeft()); //fix the first column of tdbody
  });
});

Here is a full example with your code, showing different handlings based on the browser:

https://jsfiddle.net/8tx4xfhx/5/

Alsol, it would be nice to see if anyone has noticed this behavior before. It appears that IE11 handles nested relative positioning differently than other browsers. Is there a spec somewhere that defines what the standard should be? Should relative positioning be inherited like IE does it? Or should relative positioning always be relative to the parent? I would think the latter. But performance considerations must also be taken.

Matt Spinks
  • 6,380
  • 3
  • 28
  • 47
  • Thank you, Matt. Can you say anything about the second part of the question: is it safe to use such a solution? It's using `display: flex` for `` and so on. I am afraid it contains some unspecified behavior and the solution will not work on some browsers.
    – Artem Kachanovskyi Feb 03 '17 at 08:18
  • Hi Artem. Actually, I only left the `display:flex` in there because I thought you might be using that for something else. It can be done without that attribute. Here, check this out: https://jsfiddle.net/mspinks/wLren00o/2/. This utilizes the outer container scroller. I think it's a better, more reliable solution. And it doesn't use any `flex`. – Matt Spinks Feb 03 '17 at 14:58
  • Thanks. How about `display:block` for `thead` and `tbody`? I tried the solution and it works well enough, even in IE. But on mobile browser the scrolling is not so smooth, it's possible to see that the frozen part is not really staying all the time on same place. Is there something to do with it? – Artem Kachanovskyi Feb 03 '17 at 15:12
  • I think you're safe with `display:block;` for `thead` and `tbody`. As far as the smoothness, that's probably a lag on the repositioning in a mobile browser. You will probably find that in any solution like this. – Matt Spinks Feb 03 '17 at 15:35
8

You should try below code sample with reference of jquery.floatThead.js.

                    var $demoTable = $("div.table-responsive table");
                    $demoTable.floatThead({
                        top: 200,
                        scrollContainer: function ($table) {                                
                            return $table.closest('.table-responsive');
                        },
                        position: 'absolute'
                    });

you need to get the reference of jquery.floatThead.js file and try to apply this on table.

You can check this working on below link. http://mkoryak.github.io/floatThead/

  • It's good, but it's only about the headers and it actually clones the table header to another table and positions it absolutely. – Artem Kachanovskyi Feb 16 '17 at 08:59
  • It does not clone the header, it moves it into another table (and back depending on positioning option). But yeah it doesn't do anything with column freezing – mkoryak Feb 22 '17 at 21:28
7

Generally for frozen rows & columns, I always prefer to use a css-only solution for best browser-compatibility.

I have tried to replicate your code here with a css-only solution.

I am working on a mac, so don't have access to IE. Please verify if its working fine on the same.

Updated fiddle: https://jsfiddle.net/nashcheez/bzuasLcz/81/

Refer code:

table {
  position: relative;
  width: 700px;
  background-color: #aaa;
  overflow: hidden;
  border-collapse: collapse;
}
/*thead*/

thead {
  position: relative;
  display: block;
  /*seperates the header from the body allowing it to be positioned*/
  width: 700px;
  overflow: visible;
}
thead th {
  background-color: #99a;
  min-width: 120px;
  height: 36px;
  min-height: 36px;
  border: 1px solid #222;
}
thead th:nth-child(1) {
  /*first cell in the header*/
  position: relative;
  display: block;
  background-color: #88b;
}
tbody tr td:nth-child(2) {
  margin-left: 124px;
  display: block;
}
/*tbody*/

tbody {
  display: block;
  width: 700px;
  height: 239px;
  overflow-y: auto;
}
tbody td {
  background-color: #bbc;
  min-width: 120px;
  border: 1px solid #222;
  height: 36px;
  min-height: 36px;
}
tbody tr td:nth-child(1) {
  /*the first cell in each tr*/
  position: absolute;
  display: inline-block;
  background-color: #99a;
}
<body>
  <table>
    <thead>
      <tr>
        <th>Name</th>
        <th>Town</th>
        <th>County</th>
        <th>Age</th>
        <th>Profession</th>
        <th>Anual Income</th>
        <th>Matital Status</th>
        <th>Children</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>John Smith</td>
        <td>Macelsfield</td>
        <td>Cheshire</td>
        <td>52</td>
        <td>Brewer</td>
        <td>£47,000</td>
        <td>Married</td>
        <td>2</td>
      </tr>
      <tr>
        <td>Jenny Jones</td>
        <td>Threlkeld</td>
        <td>Cumbria</td>
        <td>34</td>
        <td>Shepherdess</td>
        <td>£28,000</td>
        <td>Single</td>
        <td>0</td>
      </tr>
      <tr>
        <td>Peter Frampton</td>
        <td>Avebury</td>
        <td>Wiltshire</td>
        <td>57</td>
        <td>Musician</td>
        <td>£124,000</td>
        <td>Married</td>
        <td>4</td>
      </tr>
      <tr>
        <td>Simon King</td>
        <td>Malvern</td>
        <td>Worchestershire</td>
        <td>48</td>
        <td>Naturalist</td>
        <td>£65,000</td>
        <td>Married</td>
        <td>2</td>
      </tr>
      <tr>
        <td>Lucy Diamond</td>
        <td>St Albans</td>
        <td>Hertfordshire</td>
        <td>67</td>
        <td>Pharmasist</td>
        <td>Retired</td>
        <td>Married</td>
        <td>3</td>
      </tr>
      <tr>
        <td>Austin Stevenson</td>
        <td>Edinburgh</td>
        <td>Lothian</td>
        <td>36</td>
        <td>Vigilante</td>
        <td>£86,000</td>
        <td>Single</td>
        <td>Unknown</td>
      </tr>
      <tr>
        <td>Wilma Rubble</td>
        <td>Bedford</td>
        <td>Bedfordshire</td>
        <td>43</td>
        <td>Housewife</td>
        <td>N/A</td>
        <td>Married</td>
        <td>1</td>
      </tr>
      <tr>
        <td>Kat Dibble</td>
        <td>Manhattan</td>
        <td>New York</td>
        <td>55</td>
        <td>Policewoman</td>
        <td>$36,000</td>
        <td>Single</td>
        <td>1</td>
      </tr>
      <tr>
        <td>Henry Bolingbroke</td>
        <td>Bolingbroke</td>
        <td>Lincolnshire</td>
        <td>45</td>
        <td>Landowner</td>
        <td>Lots</td>
        <td>Married</td>
        <td>6</td>
      </tr>
      <tr>
        <td>Alan Brisingamen</td>
        <td>Alderley</td>
        <td>Cheshire</td>
        <td>352</td>
        <td>Arcanist</td>
        <td>A pile of gems</td>
        <td>Single</td>
        <td>0</td>
      </tr>
    </tbody>
  </table>
</body>
nashcheez
  • 5,067
  • 1
  • 27
  • 53
  • Thank you for your answer. I'd also want a css-only solution, but there is a problem with it: the height of rows in frozen part and the main part has to be synchronized programmatically then. See here: https://www.dropbox.com/s/3kg3cjfhrryolfj/2017-02-16_9-48-43.png?dl=0 – Artem Kachanovskyi Feb 16 '17 at 07:56
1

The problem is that IE does not allow one to adjust the left attribute of a cell in a row independent from the row as a whole. We can see this by editing the DOM directly using the developer window in IE and the developer window in Chrome.

In Chrome, when you scroll left and right, you can see in the Element viewer that the left attribute is changed on the element itself, which overrides all CSS. We can reload the page and set the element attribute manually in this same screen: style:'left:300px', and we will see the header cell move to the right 300px and hover over the remaining header cells. This is good behavior and the behavior that makes this method work.

If we do the same thing in IE and add style: 'left:300px' to the th element, we will see that the cell does not move. Nothing we do to the style attributes of that cell will cause it to leave its place in the table. It is this 'feature' of IE that is causing the method to fail. IE is insisting on maintaining cell order no matter what attributes are applied to the elements within a row.

The trick is to work around this complication in a way that makes all browsers happy. There are many ways to do this, but I would probably use two tables, and use DIVs to position them so that the edges match. I would add javascript so that if one tbody scrolls up or down that it affects the other tbody in the same manner. If it scrolls right or left, nothing happens to the first table, which holds your frozen column headers, and the right table moves in the scroll direction as planned.

By using two tables, IE no longer associates the header that you are trying to freeze with the header that is moving. Careful CSS will disguise your hack and make the table appear as one table.

Good luck and happy coding!

Jared Clemence
  • 1,062
  • 11
  • 26
  • 1
    Thank you, Jared. Actually I'm currently using the method you described. But the problem with it is that one has to synchronize the height of rows in the fixed table and the scrolling table then. And it can be annoying if the table is very dynamic: if you are loading records on demand, if the user can edit cells content, if the data can be changed dynamically and so on. I place the header in a separate div too, so in javascript I have to do a few tasks: synchronize horizontal scroll, vertical scroll, rows height. My dreams is to make it all 1 table. – Artem Kachanovskyi Feb 23 '17 at 08:02
  • That is an understandable and good dream. It is a shame that IE makes life so complicated for everyone. You might have some luck with a creative alternative. For example, perhaps instead of scrolling the divs, scroll events can be translated into different data being displayed in the tables. Javascript can be used to dynamically change the cell values to make it look like a scroll action is occurring in discrete measures, but really, the data in the cells is being changed out. – Jared Clemence Feb 24 '17 at 17:37