Create sticky table headers with CSS and jQuery

Make it easier for visitors to process large amounts of data with sticky table headers on both the X and Y axis


When dealing with large amounts of table data your users can easily become lost, so making things as readable as possible is important. Using CSS to create striped table rows and columns will allow the eye to remain in the correct place as a user scrolls through the data.

Adding sticky table headers means the headings on your table will follow the user as they scroll through the page, saving them from returning to the start of the document to find out what data each row represents.

In this tutorial we’ll look at a pure CSS option for this before moving on to a combination of both jQuery and CSS. The reason for this is that the pure CSS version is only supported by a small number of very modern browsers, by adding in jQuery we can target a larger section of visitors across all browsers. This tutorial assumes you have a development environment already configured to work on localhost or a remote development server. You will not need any server-side platforms enabled on this environment, but check your JavaScript security settings when running locally.

Generate dummy data

In order to create a table that we can scroll around in, we’ll be using a tool called Dummy Data Me, which can be found at Add at least ten rows, generating as much data as you need, and export the results to HTML. Create a blank HTML file ready for the data.

Import data

Once you have generated the table data, right-click on the page and select view-source. Unfortunately the dummy data code isn’t very well formatted, so we’ll have to tidy it up a bit before we can actually use it. Copy the source and head over to, paste the HTML in and you’ll get nicely indented table HTML ready to go into a new HTML file.

There is one more step needed before we can use the dummy data; we need to make sure that the HTML used to generate it is semantically valid. We need to ensure that the ‘thead’ and ‘tbody’ sections are defined correctly, as we will be referencing these later on in the tutorial. Add the code below around your table headings and body.

Style the table

As mentioned previously, it is important to make the table as readable as possible. In order to achieve this in a simple way, we will use some CSS to create a striped table. This CSS uses the nth-child element to only apply the style to even or odd rows, however, it is a CSS3 selector and some older browsers may not support it.

CSS sticky headers

It is possible to create sticky headers using just CSS, however, this is only supported by bleeding-edge browsers for the time being. Adding the following CSS code will enable sticky headers but until it is standardised, it should not be used in a production environment. It is unlikely you’ll be able to see the results for now, but we’ll address that in the next step.

001	    Thead {
002	        position: -webkit-sticky;
003	        position: -moz-sticky;
004	        position: -ms-sticky;
005	        position: -o-sticky;
006	        position: sticky;
007	        top: 0;
008	    }

Test the CSS

At this stage you will need to download and install either Google Chrome Canary or one of the WebKit nightly builds. We will focus on Canary as we’re developing on a Windows machine. Head over to and grab the latest build. You can run Canary alongside the standard stable version of Chrome. Once installed, run your code to view the results.

Wider support

Now we have dabbled with some bleeding edge, untested, unsupported and unstable code, it’s time to focus on supporting the other 94 per cent of users. Remove the thead CSS element that contains the sticky property and add the CDN-hosted version of jQuery to thesection of your HTML using the following code.


jQuery cloning

In order to achieve the sticky effect in jQuery, we need to clone the thead HTML elements, and present them on the page separate to the existing table. This way, we can alter the thead element, without affecting the rest of the table. The following code will do just that.


Some required CSS

Before we start making our duplicated thead sticky, we need to add some CSS that wraps around our table. Add the following code to your style section, we will programmatically apply this style to our table through jQuery. This is useful if you want to apply the sticky headers to multiple tables on the page.

001	    .wrapper {
002	        overflow-x: auto;
003	        position: relative;
004	        margin-bottom: 1.5em;
005	        width: 100%;
006	    }
007	    .wrapper .sticky-thead,
008	    .wrapper .sticky-col,
009	    .wrapper .sticky-intersect {
010	        opacity: 0;
011	        position: absolute;
012	        top: 0;
013	        left: 0;
014	        transition: all .125s ease-in-out;
015	        z-index: 50;
016	        width: auto;
017	    }
018	    .wrapper .sticky-thead {
019	        z-index: 100;
020	        width: 100%;
021	    }
022	    .wrapper .sticky-intersect {
023	        opacity: 1;
024	        z-index: 150;
025	    }
026	    .wrapper .sticky-intersect th {
027	        background-color: #666;
028	        color: #eee;
029	    }
030	    .wrapper td,
031	    .wrapper th {
032	        box-sizing: border-box;
033	    }

Wrap the table

The following code snippet will search all of the code for any tables, and then wrap it all with some CSS.

Append thead

You may have noticed that we removed the clone step from the previous step in our code, this is because we need to clone the thead and wrap it in some CSS. The following code will do exactly that and needs to replace the code you used earlier on in Step 8.

001	       $stickyHead. Append($(this).
002	       $stickyCol
003	          .append($(this).find(‘thead, 
004	          .find(‘thead th:gt(0)’).remove()
005	          .end()
006	          .find(‘tbody td’).remove();
007	       $stickyInsct.html(‘

	’+$(this).find(‘thead th:first-child’).

’); 008

Helper functions

Here we are going to add some helper functions to our JavaScript file that we will call at a later date. To keep things neat you can choose to place these functions in a separate JavaScript file or simply keep them within the same code block – it’s entirely up to you. This first function will set the widths of the columns.

001	 var setWidths = function () {
002	 $t
003	   .find(‘thead th’).each(function (i) {
004	    $stickyHead.find(‘th’).eq(i).
005	   })
006	   .end()
007	   .find(‘tr’).each(function (i) {
008	    $stickyCol.find(‘tr’).eq(i).
009	   });
010	 $stickyHead.width($t.width());
011	 $stickyCol.find(‘th’).add($stickyInsct.
	find(‘th’)).width($t.find(‘thead th’).
012	   }

Move header function

This next function will do the actual moving of the header; the if statement will check to see if the user is at the top of the page, and hide the cloned header if they are. Otherwise, it will display the cloned header and move it accordingly with a smooth scroll.

Move columns

To create the biaxial scrolling headers, we need to move the column headers as well. This next function does pretty much the same as the last, but it is applied to the column HTML, ensuring the left-hand headers are always visible on the page. You may have to add more data to your table to see this in action.

Calculate allowance

This final function will prevent the sticky header from travelling all the way to the end of the table. If we did not place this here, the final rows could be obstructed when we get to the bottom of the page. The function calculates the height of the last three rows and prevents the header from going lower than that.

001	   calcAllowance = function () {
002	    var a = 0;
003	    $t.find(‘tbody tr:lt(3)’).
	each(function () {
004	     a += $(this).height();
005	    });
006	    if(a > $w.height()*0.25) {
007	     a = $w.height()*0.25;
008	    }
009	    a += $sticky.height();
010	    return a;
011	   };

Call the functions

Now we have got all of the functions declared we can start calling them. The first will calculate all the table widths and is a single line; all the hard work is done by our function that we wrote previously. You can customise the CSS to modify the colours, but make sure you do not change any of the fundamental CSS.

Bind objects

Now we have all the widths calculated we can bind them to the window. This allows them to be called when the user scrolls down or across the page. You may notice that the below call uses the $w variable, this is a shorthand, and we’ll set this later on in the tutorial.

001	$load(setWidths)

Check for resize

As we are calculating the width of the table data, we need to implement a check that recalculates these variables if the size of the window changes. If we did not, the offset would be wrong when it comes to scrolling and our sticky headers would not sit at the top of the table.

001	    resize($.throttle(250, function () {
002	        setWidths();
003	        repositionStickyHead();
004	        repositionStickyCol();
005	    })

Call the scroll

The next call will do the repositioning of the table headers – unfortunately this will be called a great number of times and have a considerable impact on JavaScript performance. To get around this we’ll need to implement a throttle to limit the number of calls that are made.

001	.scroll(repositionStickyHead));

Implement throttling

Head to and grab the awesome JavaScript throttling library from Ben Alman. Create a new JavaScript file and paste the contents into that file. Save it and add the JavaScript reference in thesection of your index page with the following code.


Modify the JavaScript

To implement the throttle, update the line from Step 19 to the following code. At this stage we will pass a time to the throttle function as well, so now the throttle will prevent our page from spamming the jQuery functions with requests. The next step is to define our shorthand variables so that the table actually works.

001	.scroll($.throttle(250, 

Define shorthand

The final step is to hook up our shorthand JavaScript vars. The following code will set these vars at the top of the script and allow them to be referenced throughout the rest of the code. Place the following lines near the top of your JavaScript code block.

001	var $w	   = $(window),
002	       $t	   = $(this),
003	       $thead = $t.find(‘thead’).clone(),
004	       $col   = $t.find(‘thead, tbody’).