Because scrolling down a long list of tabular data without fixed column headings is annoying, to say the least.

TL;DR

Skip to the end to view source code and working demo


Most solutions found when searching for "how to create sticky table headers" typically took one of two approaches to solve this problem.

  1. Add a fixed position to the thead or first row of the table. This approach is simple and suffices if the table is the first or only element on the page. What I am after is a scroll-then-fixed solution much like this example.
  2. Use JavaScript to duplicate the first table row and place it in a new table fixed to the top of the page and reveal or hide the duplicated row depending on the viewer's scroll position. This approach feels messy and and its worth mentioning that any event listeners set on the original elements will not work on the duplicated row (unless using event delegation).

The approach I am after is a combination of the above solutions; a scroll-then-fixed solution using the original column headings to maintain event listeners.

HTML

<table>  
  <thead>
    <tr>
      <th>Col 1</th>
      <th>Col 2</th>
      <th>Col 3</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>One</td>
      <td>Two</td>
      <td>Three</td>
    </tr>
    ...
  </tbody>    
</table>  

The HTML is straightforward. Moving on...

CSS

.fixed {
  position: fixed;
  top: 0;
}

Not much to the CSS either. The .fixed class will be added to the thead element when a viewer scrolls to the table.

JS

/**
* First Calculate
*   - Where the thead lies on the page
*   - Where the table lies on the page
*   - Table height
**/
var table = document.querySelector('table'),  
    thead = table.querySelector('thead'),
    theadOffset = thead.getBoundingClientRect().top,
    tableHeight = +getComputedStyle(table)
                   .getPropertyValue('height')
                   .split('px')[0],
    tableOffset = table.getBoundingClientRect().top,
    tableCells = document.querySelectorAll('th, td');

// Set the widths on all table cells so they dont resize when fixing
// the first row
for (var i = 0, l = tableCells.length; i < l; i++) {  
  var cell = tableCells[i];  
  cell.width = getComputedStyle(el).getPropertyValue('width');
}   

function fixTableHeader(e) {  
  // If viewer has scrolled past the first row 
  // then fix/stick it to the top of the page
  if( window.pageYOffset > theadOffset ) {
    thead.classList.add('fixed');
  }

  // If viewer has scrolled back above or past the table
  // then unfreeze the first row.
  if ( window.pageYOffset < theadOffset || 
       window.pageYOffset > (tableOffset + tableHeight)) {
      thead.classList.remove('fixed');
  }

}

// Listen...
window.addEventListener('scroll', fixTableHeader, false);  

DEMO

See the Pen Fixed Table Head by Derek Worthen (@dworthen) on CodePen.