Search code examples
htmlcsshtml-tablescrollable

Table header fixed, scrollable body horizontal and vertical, variable cell width


I realize this question has been asked quite a bit on Stack Overflow; however, after looking through a number of them, I believe my question has one more requirement. I want to transform a regular html table, into a table that can be scrolled both vertically and horizontally, while the header is fixed. The table in my project is dynamically generated, so the cell width can't be fix.

Problems:

  • when scrolling left and right, the table scrolls, but the header is cut off
  • header and body cell width doesn't fit

Here is a JSFiddle to show you my problem

JSFiddle

Table example:

<div class="table-responsive">
  <table id="vehicleTable" class="table pointer hover table-striped table-bordered" role="grid">
    <thead>
      <tr>
        <th>Firstname</th>
        <th>Lastname</th> 
        <th>Age</th>
        <th>Gender</th>
        <th>Size</th> 
        <th>Mass</th>
        <th>Eyecolor</th>
        <th>Style</th> 
        <th>Shoe</th>
        <th>Sport</th>
        <th>Social Media</th>
      </tr>
     </thead>
     <tbody>
       <tr>
         <td>Jill</td>
         <td>Smith</td> 
         <td>50</td>
         <td>Female</td>
         <td>1.60</td> 
         <td>60</td>
         <td>Green</td>
         <td>Cool</td> 
         <td>New Balance</td>
         <td>Football</td> 
         <td>Facebook</td>
       </tr>
       <tr>
         <td>Anthony</td>
         <td>Meyer</td> 
         <td>34</td>
         <td>Male</td>
         <td>1.84</td> 
         <td>88</td>
         <td>Blue</td>
         <td>Elegant</td> 
         <td>All stars</td>
         <td>Golf</td> 
         <td>Facebook</td>
      </tr>
      <tr>
         <td>Eve</td>
         <td>Jackson</td> 
         <td>94</td>
         <td>Female</td>
         <td>1.58</td> 
         <td>55</td>
         <td>Green</td>
         <td>Cool</td> 
         <td>Adidas</td>
         <td>Chess</td>
         <td>Instagram</td>
      </tr>
      <tr>
         <td>Jill</td>
         <td>Smith</td> 
         <td>50</td>
         <td>Female</td>
         <td>1.60</td> 
         <td>60</td>
         <td>Green</td>
         <td>Cool</td> 
         <td>New Balance</td>
         <td>Chess</td>
         <td>Instagram</td>
       </tr>
       <tr>
         <td>Anthony</td>
         <td>Meyer</td> 
         <td>34</td>
         <td>Male</td>
         <td>1.84</td> 
         <td>88</td>
         <td>Blue</td>
         <td>Elegant</td> 
         <td>All stars</td>
         <td>Chess</td>
         <td>Instagram</td>
      </tr>
      <tr>
         <td>Eve</td>
         <td>Jackson</td> 
         <td>94</td>
         <td>Female</td>
         <td>1.58</td> 
         <td>55</td>
         <td>Green</td>
         <td>Cool</td> 
         <td>Adidas</td>
         <td>Chess</td>
         <td>Instagram</td>
      </tr>
      <tr>
         <td>Anthony</td>
         <td>Meyer</td> 
         <td>34</td>
         <td>Male</td>
         <td>1.84</td> 
         <td>88</td>
         <td>Blue</td>
         <td>Elegant</td> 
         <td>All stars</td>
         <td>Golf</td> 
         <td>Facebook</td>
      </tr>
      <tr>
         <td>Eve</td>
         <td>Jackson</td> 
         <td>94</td>
         <td>Female</td>
         <td>1.58</td> 
         <td>55</td>
         <td>Green</td>
         <td>Cool</td> 
         <td>Adidas</td>
         <td>Chess</td>
         <td>Instagram</td>
      </tr>
      <tr>
         <td>Jill</td>
         <td>Smith</td> 
         <td>50</td>
         <td>Female</td>
         <td>1.60</td> 
         <td>60</td>
         <td>Green</td>
         <td>Cool</td> 
         <td>New Balance</td>
         <td>Chess</td>
         <td>Instagram</td>
       </tr>
       <tr>
         <td>Anthony</td>
         <td>Meyer</td> 
         <td>34</td>
         <td>Male</td>
         <td>1.84</td> 
         <td>88</td>
         <td>Blue</td>
         <td>Elegant</td> 
         <td>All stars</td>
         <td>Chess</td>
         <td>Instagram</td>
      </tr>
      <tr>
         <td>Eve</td>
         <td>Jackson</td> 
         <td>94</td>
         <td>Female</td>
         <td>1.58</td> 
         <td>55</td>
         <td>Green</td>
         <td>Cool</td> 
         <td>Adidas</td>
         <td>Chess</td>
         <td>Instagram</td>
      </tr>
      <tr>
         <td>Jill</td>
         <td>Smith</td> 
         <td>50</td>
         <td>Female</td>
         <td>1.60</td> 
         <td>60</td>
         <td>Green</td>
         <td>Cool</td> 
         <td>New Balance</td>
         <td>Chess</td>
         <td>Instagram</td>
       </tr>
       <tr>
         <td>Anthony</td>
         <td>Meyer</td> 
         <td>34</td>
         <td>Male</td>
         <td>1.84</td> 
         <td>88</td>
         <td>Blue</td>
         <td>Elegant</td> 
         <td>All stars</td>
         <td>Chess</td>
         <td>Instagram</td>
      </tr>
      <tr>
         <td>Eve</td>
         <td>Jackson</td> 
         <td>94</td>
         <td>Female</td>
         <td>1.58</td> 
         <td>55</td>
         <td>Green</td>
         <td>Cool</td> 
         <td>Adidas</td>
         <td>Chess</td>
         <td>Instagram</td>
      </tr>
     </tbody>
  </table>
</div>

CSS:

table#vehicleTable {
    position: relative;
    display: block;
    margin-left: 0px;
    width: 400px;
  height: 400px;
    table-layout: fixed;
}
table#vehicleTable thead {
    position: relative;
    display: block; /*seperates the header from the body allowing it to be positioned*/
  width: inherit;
    overflow-y: hidden;
  overflow-x: hidden;
    background-color: blue;
  color: white;
}
table#vehicleTable tbody {
    position: relative;
    display: block; /*seperates the tbody from the header*/
    width: inherit;
    max-height: 400px;
    height: 400px;
    overflow-y: scroll;
    overflow-x: scroll;
}
.table-responsive {
    display: block;
    height: 400px;
    table-layout: fixed;
}
table#vehicleTable td, table#vehicleTable thead th {
    table-layout: fixed;
}

JQuery:

$(document).ready(function () {
        $('tbody').scroll(function (e) { 
            $('thead').css("left", -$("tbody").scrollLeft());
            $('thead th div:nth-child(1)').css("left", $("tbody").scrollLeft());
            $('tbody td:nth-child(1)').css("left", $("tbody").scrollLeft());
        });
    });

Solution

  • Edited to handle the possibility of longer columns, as I didn't notice that prerequisite

    Interesting problem!

    You can achieve what you want with less CSS and no jQuery, if you are happy to set a fixed width for the columns:

    If you ensure that your CSS is set so that the default column width is as long as is necessary for any table headers, then this solution should work:

    CSS

    table#vehicleTable {
      position: relative;
      display: block;
      margin-left: 0px;
      width: 400px;
      height: 400px;
      overflow-x: scroll;
      overflow-y: hidden;
    }
    table#vehicleTable thead {
      background-color: blue;
      color: white;
    }
    table#vehicleTable tbody {
      position: relative;
      display: inline-block;
      height: 345px; /*allowing for scrollbar and th height */
      overflow-y: scroll;
    }
    table#vehicleTable td, table#vehicleTable th  {
      min-width: 100px; /*suitable width for all column headers in the example*/
    }
    
    /*Added this just to handle the 2nd column, which needs to be smaller, as per comments */
    
    table#vehicleTable td:nth-of-type(2), table#vehicleTable th:nth-of-type(2) {
      min-width: 30px;
      text-align: center;
    }
    

    jQuery

    $(window).on('load', function () {
        $i = 0;
        $("#vehicleTable tbody tr:first-of-type td").each(function()
      {
        $i++;
        width = $(this).outerWidth();
        $("#vehicleTable thead th:nth-of-type(" +$i+")").css({"min-width": width+"px"});
      });
    });
    

    This will loop through the first row of tbody, and get the respective widths for each column. If you don't set a min-width on them first, then any columns with data that is shorter than the column header will result in that column shrinking, while the header itself will still expand to fit its own content.

    Also updated JSFiddle