Steven Berridge

Web Developer

Fixed Table Headers

Using JavaScript combined with some CSS trickery we're going to create tables with headers that stick to the top of the screen as you scroll down so they are always visible. This will be very useful for pages with large tables with lots of data as your users will always be able to see what is being represented in each column.

Example

To begin with, here's an example of what we will be creating.

Column 1 Column 2 Column 3 Column 4 Column 5
234234 534456 1213 12 67868
456 768678 23489 23402034 1312398
4534 6756 1234 678678 234
5757 2342 6878 3453 678
5675 7896 3435 6867345 7896645
456756 235336 7896 3453 6875
234234 534456 1213 12 67868
456 768678 23489 23402034 1312398
4534 6756 1234 678678 234
5757 2342 6878 3453 678
5675 7896 3435 6867345 7896645
456756 235336 7896 3453 6875
5675 7896 3435 6867345 7896645
456756 235336 7896 3453 6875
234234 534456 1213 12 67868
456 768678 23489 23402034 1312398
4534 6756 1234 678678 234
5757 2342 6878 3453 678
5675 7896 3435 6867345 7896645
534545 235336 7896 3453 6875

The HTML

The HTML for this is very simple, all you need is a table. The more data you have in your table the better this will work since you need to be able to scroll through the data.

<table>
	<thead>
		<tr>
			<th>Column 1</th>
			<th>Column 2</th>
			<th>Column 3</th>
			<th>Column 4</th>
			<th>Column 5</th>
		</tr>
	</thead>
	<tbody>
		<tr>
			<td>234234</td>
			<td>534456</td>
			<td>1213</td>
			<td>12</td>
			<td>67868</td>
		</tr>
		<tr>
			<td>456</td>
			<td>768678</td>
			<td>23489</td>
			<td>23402034</td>
			<td>1312398</td>
		</tr>
		<tr>
			<td>4534</td>
			<td>6756</td>
			<td>1234</td>
			<td>678678</td>
			<td>234</td>
		</tr>
		<tr>
			<td>5757</td>
			<td>2342</td>
			<td>6878</td>
			<td>3453</td>
			<td>678</td>
		</tr>
	</tbody>
</table>

The CSS

We'll be creating a copy of the table and overlaying it on top of the original table to create the illusion of the table headings moving down the screen as you scroll. In order to do this we will need to make sure the body of the cloned table is hidden. We'll mostly be doing this using CSS.


.fixed-heading-container {
	position: relative;
}
table.fixed-heading {
	position: relative;
	z-index: 1;
}
table.fixed-heading-clone {
	position: absolute;
	top: 0;
	z-index: 2;
	transition: top .1s;
}
table.fixed-heading-clone tbody {
	position: relative;
	z-index: -1;
}
table.fixed-heading-clone tbody td {
	position: relative;
	z-index: -1;
	line-height: 0;
	vertical-align: top;
	border-color: rgba(0,0,0,0);
	opacity: 0;
	padding-top: 0px;
	padding-bottom: 0px;
	border-top: none;
	border-bottom: none;
	height: 0px;
}
table.fixed-heading-clone tbody td * {
	height: 0px;
}

The fixed-heading-container class represents a div element which will contain the table and it's clone, this is given a position relative style so that the cloned table can be positioned absolutely relative to this container.

The fixed-heading class is given to our original table. This is positioned relatively so that we can set a z-index which will allow the cloned table to be displayed on top of the original table.

The fixed-heading-clone class is given to the clone of the original table. This is positioned absolutely and given a z-index placing it above the original table. We're also giving this a transition style to smooth out the movement of the header as you scroll.

The body of the clone is positioned relatively and given a negative z-index to place it below everything else, this is part of the effort to remove the body of the clone from view.

The cells of the cloned body are removed from view using various styles which essentially remove all of their height. This is done by setting the line height to 0, removing all vertical padding and borders, and setting the vertical-align property to "top". We also set the height of any elements which might be inside the cells to 0, which removes any height from things such as images.

The JavaScript

We'll be using JavaScript to handle the creation of the table clone as well as it's position as we scroll down the page. We're going to create a JavaScript class to handle this.

function FixedHeadTable(table) {
	var table = document.getElementsByTagName('table')[0],
		tbody = table.getElementsByTagName('tbody')[0],
		rows = tbody.getElementsByTagName('tr');

	if(!table.classList.contains('fixed-heading')) {
		table.classList.add('fixed-heading');
	}

	var container = document.createElement('div');
	container.classList.add('fixed-heading-container');
	table.parentNode.insertBefore(container,table);
	container.appendChild(table);

	var clonedTable = table.cloneNode(1);

	clonedTable.setAttribute('aria-hidden','true');
	
	clonedTable.classList.add('fixed-heading-clone');

	container.insertBefore(clonedTable,table);

	var offsetTop = 0;
	function calculateOffset() {
		var bounding = table.getBoundingClientRect(),
		bodyBounding = document.body.getBoundingClientRect();

		offsetTop = bounding.top - bodyBounding.top;
	}

	function manageHeader() {
		var scrollY = window.scrollY;
		var diff = offsetTop - scrollY;

		if(diff < 0) {
			diff = diff*-1;
			if(diff+clonedTable.offsetHeight > container.offsetHeight) {
				diff = container.offsetHeight-clonedTable.offsetHeight;
			}
			clonedTable.style.top = diff+'px';
			clonedTable.style.display = '';
		} else {
			clonedTable.style.display = 'none';
		}
	}

	calculateOffset();
	manageHeader();

	window.addEventListener('scroll',manageHeader);
	window.addEventListener('resize',function() {
		calculateOffset();
		manageHeader();
	});
}

This class accepts a table element as it's only parameter, we use this to access the tbody and rows in the table which we store in the tbody and rows variables.

If the table doesn't already have the "fixed-heading" class then we add it.

Next we create the containing div to contain our table and it's clone, we give this the "fixed-heading-container" class and insert it into the dom before the table using the insertBefore function, we then move the table into this div using appendChild.

The next step is to create the clone of our table, this is done using cloneNode and is stored in the clonedTable variable.

By setting the aria-hidden attribute on the cloned table we can ensure that the cloned table will be hidden from any screen readers which might be viewing the page.

We give the cloned table the fixed-heading-clone class then insert it into the containing element before the original table.

In order to position the cloned table correctly we need to know how the distance between the top of the table and the top of the page, to do this we have a function called "calculateOffset". This uses the getBoundingClientRect functions on the table and body of the page to calculate the distance between the table and the top of the page, this distance is stored in the offsetTop variable.

The manageHeader function calculates how far we've scrolled down the page, whether or not we've scrolled passed the table, and where the cloned table should be placed so that it appears at the top of the screen, then sets the appropriate styles so it is displayed correctly. If we haven't scrolled far enough down the page for the headers to appear then the cloned table is hidden.

Lastly we add event listeners which run whenever the page is scrolled or resized which run the manageHeader and calculateOffset functions.

We can then use this class by creating a new instance of it, passing it a table element.

new FixedHeadTable(document.getElementById('table'));