CSS-Only Scrollable Table with fixed headers

Viewed 184451

I have a solution by which I can create scrollable tables w/fixed header/footer using minor jQuery and CSS - but I am looking for a way to make this a CSS-only solution that is cross-browser compliant.

To be clear, what I am seeking to do is use only a table tag (and it's valid sub-tags, colgroup, col, thead, tbody, tfoot, tr, th, td), but adopt a set of CSS rules which will meet the following conditions:

  1. Must maintain column alignment between header / footer / content rows
  2. Must allow the header/footer to remain fixed while the content scrolls vertically
  3. Must not require any jQuery or other JavaScript in order to provide the functionality
  4. Must only use the tags provided above

This code example: http://jsfiddle.net/TroyAlford/SNKfd/ shows my current approach. Most of the JS is just to populate the table with random values, but the last portion is what drives the left/right scrollability.

$tbody.bind('scroll', function(ev) {
    var $css = { 'left': -ev.target.scrollLeft };
    $thead.css($css);
    $tfoot.css($css);
});

NOTE: The example provided does not render properly in IE, and requires jQuery to provide the horizontal scrolling. I don't care about horizontal scrolling anyway, so it's fine if a solution doesn't do that.

13 Answers

This answer will be used as a placeholder for the not fully supported position: sticky and will be updated over time. It is currently advised to not use the native implementation of this in a production environment.

See this for the current support: https://caniuse.com/#feat=css-sticky


Use of position: sticky

An alternative answer would be using position: sticky. As described by W3C:

A stickily positioned box is positioned similarly to a relatively positioned box, but the offset is computed with reference to the nearest ancestor with a scrolling box, or the viewport if no ancestor has a scrolling box.

This described exactly the behavior of a relative static header. It would be easy to assign this to the <thead> or the first <tr> HTML-tag, as this should be supported according to W3C. However, both Chrome, IE and Edge have problems assigning a sticky position property to these tags. There also seems to be no priority in solving this at the moment.

What does seem to work for a table element is assigning the sticky property to a table-cell. In this case the <th> cells.

Because a table is not a block-element that respects the static size you assign to it, it is best to use a wrapper element to define the scroll-overflow.

The code

div {
  display: inline-block;
  height: 150px;
  overflow: auto
}

table th {
  position: -webkit-sticky;
  position: sticky;
  top: 0;
}


/* == Just general styling, not relevant :) == */

table {
  border-collapse: collapse;
}

th {
  background-color: #1976D2;
  color: #fff;
}

th,
td {
  padding: 1em .5em;
}

table tr {
  color: #212121;
}

table tr:nth-child(odd) {
  background-color: #BBDEFB;
}
<div>
  <table border="0">
    <thead>
      <tr>
        <th>head1</th>
        <th>head2</th>
        <th>head3</th>
        <th>head4</th>
      </tr>
    </thead>
    <tr>
      <td>row 1, cell 1</td>
      <td>row 1, cell 2</td>
      <td>row 1, cell 2</td>
      <td>row 1, cell 2</td>
    </tr>
    <tr>
      <td>row 2, cell 1</td>
      <td>row 2, cell 2</td>
      <td>row 1, cell 2</td>
      <td>row 1, cell 2</td>
    </tr>
    <tr>
      <td>row 2, cell 1</td>
      <td>row 2, cell 2</td>
      <td>row 1, cell 2</td>
      <td>row 1, cell 2</td>
    </tr>
    <tr>
      <td>row 2, cell 1</td>
      <td>row 2, cell 2</td>
      <td>row 1, cell 2</td>
      <td>row 1, cell 2</td>
    </tr>
    <tr>
      <td>row 2, cell 1</td>
      <td>row 2, cell 2</td>
      <td>row 1, cell 2</td>
      <td>row 1, cell 2</td>
    </tr>
  </table>
</div>

In this example I use a simple <div> wrapper to define the scroll-overflow done with a static height of 150px. This can of course be any size. Now that the scrolling box has been defined, the sticky <th> elements will corespondent "to the nearest ancestor with a scrolling box", which is the div-wrapper.


Use of a position: sticky polyfill

Non-supported devices can make use of a polyfill, which implements the behavior through code. An example is stickybits, which resembles the same behavior as the browser's implemented position: sticky.

Example with polyfill: http://jsfiddle.net/7UZA4/6957/

Inspired by @Purag's answer, here's another flexbox solution:

/* basic settings */
table { display: flex; flex-direction: column; width: 200px; }
tr { display: flex; }
th:nth-child(1), td:nth-child(1) { flex-basis: 35%; }
th:nth-child(2), td:nth-child(2) { flex-basis: 65%; }
thead, tbody { overflow-y: scroll; }
tbody { height: 100px; }

/* color settings*/
table, th, td { border: 1px solid black; }
tr:nth-child(odd) { background: #EEE; }
tr:nth-child(even) { background: #AAA; }
thead tr:first-child { background: #333; }
th:first-child, td:first-child { background: rgba(200,200,0,0.7); }
th:last-child, td:last-child { background: rgba(255,200,0,0.7); }
<table>
    <thead>
      <tr>
        <th>a
        <th>bbbb
    <tbody>
      <tr>
        <td>fooo vsync dynamic
        <td>bar
      <tr>
        <td>a
        <td>b
      <tr>
        <td>a
        <td>b
      <tr>
        <td>a
        <td>b
      <tr>
        <td>a
        <td>b
      <tr>
        <td>a
        <td>b
      <tr>
        <td>a
        <td>b
  </table>

Another implementation but without any overflow on tbody and dynamic columns. Requires JavaScript though. A container div is used to house the column headings. When the table is scrolled past the view port, a fixed header appears at the top. If table is scrolled horizontally, the fixed header scrolls as well.

Column headings are created using span elements with display: inline-block and a negative margin is used to scroll header horizontally. Also optimized using RequestAnimationFrame to avoid any jank.

function rAF(scrollLeft) {
  var offsetLeft = 0 - scrollLeft;
  $('.hdr__inner span:first-child').css('margin-left', offsetLeft);
}

https://codepen.io/lloydleo/pen/NRpqEE

Related