Sticky Table Headers & Columns

A tutorial on how to create sticky headers and columns for tables using jQuery. The solution is an alternative to other sticky table header approaches and it addresses the overflowing table problem including adding support for biaxial headers.

Sticky table headers are no longer a stranger to an average website user — unlike on paper when a reader’s eyes can comfortably jump in saccades between top of a lengthy table and the rows of interest, the landscape orientation of most devices makes vertically-long tables hard to read. However, this very conundrum presents itself as a rich ground for UI experimentation that is not available to the printed media.

Sticky table headers, as their name implies, remains affixed to the top of the viewport even when the original table headers are scrolled out of view. They help to clarify the representation and purpose of data in columns when the visual reference to original table headers was lost. Besides that, the aid in orienting users in a sea of tabulated information, therefore avoiding the need to repeatedly, and frustratingly, scroll between the top of the table where the header resides, and the region of interest further down, typically lying out of the viewport.

There have been a handful of scripts and jQuery plugins written for the purpose of re-establishing the flow and ease of reading tables. While their implementation is flawless and efficient, they might not be an all-encompassing panacea for long tables. In some cases, tables have to obey certain layout rules that are not accounted for by the aforementioned plugins — such as tables that are forced to overflow due to dimension restrictions (e.g. to fit within a viewport).

While this tutorial does not try to serve as an all-encompassing panacea to the decidedly sticky problem with sticky table headers, it addresses more possible layout scenarios.

A pure CSS-based solution with position: sticky?

Last September, a somewhat promising solution surfaced — a new possible value for the CSS position property is supported in the latest nightly build of WebKit or Chrome Canary. position: sticky sounded like a very promising new JS-free solution to the old and nuance problems of rigid table headers and beyond — its implementation can be also extremely useful in scenarios where a site navigation or a HTML5 app toolbar has to remain in view to the user at all times regardless of his/her scroll position along the document’s y-axis.

Moreover, the sticky property value is supported in barely 6% of all global visits, making it a poor candidate for choice of implementation. Although it will not break layouts as browsers are dictated by W3C directive to ignore properties values that are unrecognized, invalid or illegal, it is not an ideal candidate when cross-browser functionality is desired.

The jQuery-based solution

The jQuery-based solution is rather straight-forward. Before we move on with the JS itself, we should come to a common consensus how a semantically valid table should look like in the markup:

            <!-- more columns are possible -->
            <!-- more columns are possible -->
        <!-- more rows are possible -->
    <tfoot><!-- optional -->

What do we want to achieve?

We should enumerate the expectations of this script. It would be great if the script can accommodate various table layouts and situations:

  • Basic usage: Sticky table header only
  • Biaxial table headers
  • Wide tables:
    • Horizontal overflow: If there is a row header, we should introduce a sticky table column, too
    • Vertical overflow: Covered in basic usage
    • Biaxial overflow: Introduce sticky table header and column

Some CSS groundwork

Despite choosing to work with a JS-based solution, we will still have to rely on CSS for the basic styling of the headers. The important things is that we have to position the sticky header absolutely within a common parent with its full-fledge, original table sibling. The CSS is rather straight forward:

.sticky-wrap {
    overflow-x: auto;
    position: relative;
    margin-bottom: 1.5em;
    width: 100%;
.sticky-wrap .sticky-thead,
.sticky-wrap .sticky-col,
.sticky-wrap .sticky-intersect {
    opacity: 0;
    position: absolute;
    top: 0;
    left: 0;
    transition: all .125s ease-in-out;
    z-index: 50;
    width: auto; /* Prevent table from stretching to full size */
    .sticky-wrap .sticky-thead {
        box-shadow: 0 0.25em 0.1em -0.1em rgba(0,0,0,.125);
        z-index: 100;
        width: 100%; /* Force stretch */
    .sticky-wrap .sticky-intersect {
        opacity: 1;
        z-index: 150;
    .sticky-wrap .sticky-intersect th {
        background-color: #666;
        color: #eee;
.sticky-wrap td,
.sticky-wrap th {
    box-sizing: border-box;

Note: It is extremely important that you port the styles for your <table> elements over to .sticky-wrap. Although margins of pixel values can be easily calculated and applied to the new wrapper element, automatic margins are difficult to deal with (it is not possible to fetch the value of auto with jQuery in a straightforward manner), and it is easier if we simply apply the margins and width of tables to the wrapper element itself.

Let’s say you have the following styles for your table:

table {
    margin: 0 auto 1.5em;
    width: 75%;

You can simply add the lines to “.sticky-wrap”, too:

.sticky-wrap {
    overflow-x: auto; /* Allows wide tables to overflow its containing parent */
    position: relative;
    margin: 0 auto 1.5em;
    width: 75%;

I shall walk you through the steps that will, with a dozens of lines of JavaScript, create functional sticky table headers. For the ease of presentation, the script is presented in a logical flow towards problem solving — declaration of variables with the var statement can definitely be concatenated for a more compact and compressed script, but at the sake of logical flow and readability, therefore I have chosen not to adopt the latter approach.

We shall execute our function for every single instance of table selected for upon DOM ready. Moreover, we will also want to check if the selected tables contain the <thead> element, and that the <thead> element is not empty and contains at least one <th> child. If the aforementioned criteria are not satisfied, our function will simply skip that instance of <table> and move on to the next.

$(function () {
    // Here we select for <table> elements universally,
    // but you can definitely fine tune your selector
    $('table').each(function () {
        if($(this).find('thead').length > 0 && $(this).find('th').length > 0) {
            // Rest of our script goes here

Step 1: Clone the <thead> element

Before we start, we will want to close the table head, and declare some shorthand variables for ease of use:

// Declare variables and shorthands
    var $t     = $(this),
        $w     = $(window),
        $thead = $(this).find('thead').clone(),
        $col   = $(this).find('thead, tbody').clone();

Step 2: Wrap table and append new tables

In order to extend compatibility towards tables that have excessive width along the x-axis (e.g. having too many columns, or columns that are necessarily yet excessively wide), we wrap the table elements in a <div> element that is allowed to overflow along the x-axis. The width and margin properties are reset for the table so as to allow proper display within the wrapper.

// Wrap table
    margin: 0,
    width: '100%';
.wrap('<div class="sticky-wrap" />');

// Check if table is set to overflow in the y-axis
if($t.hasClass('overflow-y')) $t.removeClass('overflow-y').parent().addClass('overflow-y');

// Create new sticky table head (basic)
$t.after('<table class="sticky-head" />')

// If <tbody> contains <th>, then we create sticky column and intersect (advanced)
if($t.find('tbody th').length > 0) {
    $t.after('<table class="sticky-col" /><table class="sticky-intersect" />');
// Create shorthand for things
var $stickyHead  = $(this).siblings('.sticky-thead'),
    $stickyCol   = $(this).siblings('.sticky-col'),
    $stickyInsct = $(this).siblings('.sticky-intersect'),
    $stickyWrap  = $(this).parent('.sticky-wrap');

Step 3: Inserting cloned table contents

The trick now is to insert contents cloned from our original table into the newly created tables that will serve as our sticky elements:

  1. Sticky header will receive all contents from the cloned <thead> element
  2. Sticky column will receive contents from the first <th> from <thead>, and all the subsequent <th> from <tbody>. This is assuming that each row only contains one table header cell.
  3. Sticky intersect will receive content from the top left most cell in the table
// Sticky header gets all content from <thead>

// Sticky column gets content from the first <th> of both <thead> and <tbody>
    .find('thead th:gt(0)').remove()
    .find('tbody td').remove();

// Sticky intersect gets content from the first <th> in <thead>
$stickyInsct.html('<thead><tr><th>'+$t.find('thead th:first-child').html()+'</th></tr></thead>');

Step 4: Functions

Here comes the most important part of our jQuery script — we decide what functions are needed for sticky headers to work, and we declare them with the var statement to allow for easy callback. Two functions immediately come to mind:

  1. A function to determine the widths of individual <th> elements in the cloned header. Since we only cloned the <thead> element, the computed width of the cloned header will not be the same as the actual header itself, since the content of <tbody> itself, which may or may not influence the final width of each individual columns, is not included.
  2. A function to position the sticky header, so that we can update the vertical offset of the cloned header that is absolutely positioned when the scroll event is fired.
  3. A function to position the sticky column, so that we can update the horizontal offset when the parent element is overflowing.
  4. A function to calculate allowance — this feature is explained later in greater detail.

You may ask, why do I have to calculate the vertical offset of the header instead of simply using position: fixed? I, too, have contemplated over this issue, but it came to my realization that if we are allowing the table to overflow along the x-axis, the fixed positioning option has to go out of the window, because it will not scroll with the table in the event of a horizontal overflow.

// Function 1: setWidths()
// Purpose: To set width of individually cloned element
var setWidths = function () {
        .find('thead th').each(function (i) {
        .find('tr').each(function (i) {

        // Set width of sticky table head

        // Set width of sticky table col
        $stickyCol.find('th').add($stickyInsct.find('th')).width($t.find('thead th').width())

// Function 2: repositionStickyHead()
// Purpose: To position the cloned sticky header (always present) appropriately
    repositionStickyHead = function () {
        // Return value of calculated allowance
        var allowance = calcAllowance();

        // Check if wrapper parent is overflowing along the y-axis
        if($t.height() > $stickyWrap.height()) {
            // If it is overflowing
            // Position sticky header based on wrapper's scrollTop()
            if($stickyWrap.scrollTop() > 0) {
                // When top of wrapping parent is out of view
                    opacity: 1,
                    top: $stickyWrap.scrollTop()
            } else {
                // When top of wrapping parent is in view
                    opacity: 0,
                    top: 0
        } else {
            // If it is not overflowing (basic layout)
            // Position sticky header based on viewport scrollTop()
            if($w.scrollTop() > $t.offset().top && $w.scrollTop() < $t.offset().top + $t.outerHeight() - allowance) {                 // When top of viewport is within the table, and we set an allowance later
                // Action: Show sticky header and intersect, and set top to the right value
                    opacity: 1,
                   top: $w.scrollTop() - $t.offset().top
             } else {
                 // When top of viewport is above or below table
                 // Action: Hide sticky header and intersect
                     opacity: 0,
                     top: 0
// Function 3: repositionStickyCol()
// Purpose: To position the cloned sticky column (if present) appropriately
    repositionStickyCol = function () {
        if($stickyWrap.scrollLeft() > 0) {
            // When left of wrapping parent is out of view
            // Show sticky column and intersect
                opacity: 1,
                left: $stickyWrap.scrollLeft()
        } else {
            // When left of wrapping parent is in view
            // Hide sticky column but not the intersect
            // Reset left position
            .css({ opacity: 0 })
            .add($stickyInsct).css({ left: 0 });
// Function 4: calcAllowance()
// Purpose: Return value of calculated allowance
     calcAllowance = function () {
         var a = 0;

         // Get sum of height of last three rows
         $t.find('tbody tr:lt(3)').each(function () {
             a += $(this).height();

         // Set fail safe limit (last three row might be too tall)
         // Set arbitrary limit at 0.25 of viewport height, or you can use an arbitrary pixel value
         if(a > $w.height()*0.25) {
            a = $w.height()*0.25;

        // Add height of sticky header itself
        a += $sticky.height();

        return a;

Now, you may ask, what is allowance? What do we need it for? The basis of the allowance is simple — we do not want the sticky table header to follow us all the way to the end of the table, do we? It is unnecessary, and run the risk of obfuscating the last table row. While this feature is optional (thus allowance is set to 0, see above), I highly recommend allowing at least one table row of height remaining. The height can be computed from the table itself, or you can set a fixed height.

As far as my experience go, I realize that I do not need the header much after the last three rows of the table is shown — that is because by then our eyes would have probably moved away from the table into the content below. This threshold is arbitrary, and it is up to you to decide.

// Calculate allowance
// We allow the last three rows to be shown without the need for the sticky header to remain visible
$t.find('tbody tr:lt(4)').each(function () {
    allowance += $(this).height();

Step 5: Fire away, fire away!

Now we are done declaring all the functions we need for the correct styling and positioning of the sticky header. All is left is to bind event handlers to the $(window) object (previously abbreviated as $w for your convenience), and trigger the right function. Here is the game plan:

  1. When the DOM is ready, perform initial round of width calculations
  2. When all resources are loaded, perform second round of width calculations. This is important especially when your table contains resources that are loaded after DOM ready event, such as images, @font-face and more, which will influence how table column widths are computed.
  3. When the parent wrapper is scrolled, but this only happens if the content is overflowing. In the event of a scrolling event is detected, we want to reposition the sticky column
  4. When the viewport is resized, we want to recompute widths and reposition the sticky header
  5. When the window is scrolled, we want to reposition the sticky header

This can be easily summarized with the code below. Do note that the resize and scroll events are debounced and throttled respectively using Bel Alman’s jQuery throttle+debounce plugin.

// #1: When DOM is ready (remember, we have wrapped this entire script in $(function(){...});

// #2: Listen to scrolling event on the parent wrapper (will fire if there is an overflow)
$t.parent('.sticky-wrap').scroll($.throttle(250, function() {

// Now we bind events to the $(window) object
// #3: When all resources are loaded
// #4: When viewport is resized
// (we debounce this so successive resize event is coalesced into one event)
.resize($.throttle(250, function () {
// #5: When the window is scrolled
// (we throttled this so scroll event is not fired too often)
.scroll($.throttle(250, repositionStickyHead);

And voila, you’re done!


No tutorial is complete without a discussion — be it addressing potential drawbacks on the technicalities of implementation, or the explanation of my strategy in contrary to common expectations.

Why don’t you use position: fixed?

Fixed positioning is a very tantalizing alternative, mainly because of the two advantages it confers:

  1. No calculations are needed for the sticky table header’s vertical offset, because fixed position will cause the element to be positioned absolutely within the viewport and out of the document flow, and
  2. Avoids the stuttering effect of the sticky table header playing catch up, as the scroll event is throttled and therefore the calculations are performed at fixed time intervals and not on the fly. It may appear less responsive to user movement and therefore less natural.

However, the issue with fixed positioning is that we are effectively removing the element from the document flow. In the event when the table width exceeds that of its containing and a horizontal overflow is absolutely necessary, the fixed position header will not scroll with the table because it is detached from the document layout. This is one of the major drawbacks with many jQuery plugins out there that offers sticky table header functionality, and this tutorial was written partially to address this issue.

Why don’t you use position: sticky?

The new position attribute, position: sticky, is only available to the latest version of WebKit browsers and require vendor prefixes. It is not supported in Firefox, Internet Explorer and Opera, therefore risking alienating a huge user base (~95%) due to lack of support. It is not official and standardized as of yet, so I would rather err on the side of caution, and choose a more cumbersome but cross-browser friendly JS-based solution.

Tagged with:

Terry Mun

Amateur photographer, enthusiastic web developer, whimsical writer, recreational cyclist, and PhD student in molecular biology. Sometimes clumsy.

Stay in the loop: Get your dose of frontend twice a week

👾 Hey! Looking for the latest in frontend? Twice a week, we'll deliver the freshest frontend news, website inspo, cool code demos, videos and UI animations right to your inbox.

Zero fluff, all quality, to make your Mondays and Thursdays more creative!

Feedback 72

Comments are closed.
  1. I love Codrops demos and use them quite often, but this one works really poor, at least on Windows 7 in latest FF and Chrome.
    I also love animation in UI, but IMHO this is not the right case for it. I may miss something, but completely static headers in Google Docs works way more predictable and understable for me.

    • Hi Dmitry,
      I think you are missing the main point here, which is that the purpose of this script is to provide a sticky table header solution. This is not a demonstration of a fancy transition effect but a practical approach to a very tricky layout – large data tables. Viewing large data tables and scanning through them does not happen with rapid scrolling. What’s important here is the functionality of the sticky header itself making it easy to keep track of the labels while looking through the data. I think Terry did an excellent job in providing a solution like this and explaining the challenges and workings of it. He also explains why the movement is not as smooth as you are probably expecting. That’s my two cents. I hope this makes sense to you. Thanks for your feedback. Cheers, ML.

    • I appreciate the criticism, although it will be great if you have elaborated further on the problem you have faced — it was not after you have posted the YouTube video that I realized what you were refering to.

      That stuttering positioning effect is unavoidable due to the avoidance of using position: fixed, as it will not be compatible with tables that are overflowing along the x-axis. This part has been covered in the discussion towards the end of the tutorial.

      However, speaking of which I might have a little light bulb that went on in my head — I’ll get back to you when I have got the solution for that.

    • Mary and Terry,

      First of all, thank you for your response. I realize that viewing large tables is a challenge but I really can’t say this demo is a solution, though it can be really nice and tricky in terms of coding.

      As I have already mentioned, Google Docs works just fine. If this is not relevant, I can suggest to take a look on sticky header in JIRA. May be I’m missing something, but if you can’t achieve same quality with several tables or biaxial tables, then you probably should not post a semi working solution. At least not on Codrops, which is a source of excellent web solutions for me.

      This being said, I want to point major issues I see in this demo. I thought they were pretty obvious in the Video but let’s go over it. In one word – it’s laggy.
      Scrolling down gives unpleasant effect, when header first disappears and that appears back with animation.
      Scrolling up works even worse – header is not fixed at all. Once again – header in demo of fixed header is not fixed. Pretty strange, huh? Surprisingly you can also get to a situation, when you see 2 headers at the same. Again – are we still speaking about solution for one fixed header ?
      And last point is about visual – I would never recommend use same color for both header and highlight. It’s just frustrating.
      PS Mary, I couldn’t reach you via comments on Codrops and Facebook with a question about your awesome NY demo. I have used it on a site of Russian game development studio (only Russian, sorry). If it’s not legal, please, let me know, I’ll remove it.

    • I’ve to agree with Dmitry on this one. And I experience this same problems that he mentioned on Chrome for Mac.
      I don’t think this could be called a solution for sticky table headers, it’s more like a WIP to reach the solution – specially the annoyance of the header showing and disappearing as you scroll.

  2. Hi, this is a great job ^^

    It is possible to perform the same effect with th column on the right side instead of the left?


    • That is actually a great point, Hector! The reason for not putting the sticky column on the right tho, is because the purpose of scrolling towards the right is to reveal content that is hidden or have overflown its parent container – by placing the sticky column on the right, it defeats the purpose of scrolling, as the user will have to scroll more than they intend to in order to reveal the column they need to see, don’t you think so?

      If you want to achieve this effect, you can simply position the sticky column on the right. Instead of using the following line:

      left: $stickyWrap.scrollLeft()

      … you can try using:

      left: $stickyWrap.scrollLeft() + $stickyWrap.width() - $stickyCol.width()

      What we are doing is to add the parent width to the scroll with, so to push it towards the right; then minus off the width of the sticky column itself (or else it will be off screen).

    • Thanks, I was looking around that area but could not find how to modify it, I have to give a few tweaks but it works perfectly. I was trying to maintain a column of actions (edit, delete, etc.) for each row that was fixed regardless of the number of columns and looks great!

      And, yes, it is generally better to have the sticky column to the left;-)

      Greetins (and sorry for my english, i’m spanish jaja)

  3. It lags smooth transition. You have done lot of calculations, but it loads only after scroll is stopped.

  4. Nice concept, though JS is a bit laggy. I’d much rather do this in pure CSS displaying the body as a block (in self contained scrollable tables). Great solution for tables displayed whole though.

  5. I’m sorry but this is too retro. It’s an awful effect and very inefficient. It’s what we used to have to put all over the place pre “position: fixed” being supported and that was a very long time ago. Position fixed solved all this a long time ago and I’ve grudgingly written code like you have here to support IE6 as a polyfill. I wouldn’t choose to abuse a nice modern browser with this. What’s going on Codrops? I’m usually blown away by all the cool stuff you put up.

    • I apologize that the technique used was awful and retro – the demo is in the process of being updated for a better effect, and the script is in the process of being ported over into a GitHub repo. This post will be updated as soon as the new demo is running.

    • Have you ever tried to use a table header in combination with position fixed? It’s a mess and you will never get it running pixel perfect even in modern browsers.

  6. This is great. I know a current project that would benefit greatly from this technique. Thanks very much for sharing!

  7. Per the MDN documentation for position, Chrome standard supports it, Safari 6.1 and above supports it with the -webkit- prefix, and Firefox 26 and above supports it if you set the about:config preference to layout.css.sticky.enabled = true.

    With Opera using the Blink engine now, we should hopefully see it adopted there soon, and hopefully Firefox should bring it into the mainline version by v28. Then we can just go back to our SOP of IE-exclusive polyfills.

  8. Very nice !

    I like the sticky column !

    My solution is pure JS and has only a floating header.
    You can find it at

    When there is time I will work on a floating column.


  9. Ive always wanted to freeze rows/columns in html tables. It seemed like too much work but i’ll try out this method.

  10. Brilliant. Learned a lot from this post. I always wanted to know how to do that. Thanks for sharing.

  11. Really it is nice post. Learned a lot from this post. I always wanted to know how to do that. Thanks for sharing.

  12. How would I init this javascript? Actually I am refreshing the container div using ajax call every 10 seconds and after that refresh, StickyTable JS doesn’t work at all somehow.. So I am thinking I need to init the js..

    • As far as I can see, the problem with this approach is that it doesn’t work well with tables too wide to fit within the outer div. You end up with two horizontally scrollable independent divs and would need to synchronize the scrolling among the two.

  13. Hi,

    really nice tutorial and solution. Although CSS only solution would be really nice, but to my knowledge it’s not possible to get it working with both headers and columns fixed.

    The only issue I have right now is IE9 bug. Biaxal header expample doesn’t work properly. When scrolling vertically or horizontally additional space get added to bottom. Then it’s removed. Video link:

    Would be great to get any ideas how to fix it.


    • I’m noticing the same issue in IE9 as well and it appears that either we’re the only ones that notice this and is a problem, or no one has fixed this bug yet. This would not be good if not working properly in IE9 as the user base unfortunately is very large with this browser/version and would make this plugin useless to implement, any takers on why this is happening and how to resolve?

    • Just curious as to why my comment was deleted, was confirming that this is still an issue in IE9 that’s all, hmmm!

    • Hi Nick, I am seeing 2 of your comments approved for this article. Is there another one missing? Cheers.

  14. Amazing!
    How would one modify this demo to have two fixed columns? Let’s say first column is “item name” and last column is “total”. You want to allow scrolling horizontally but for both columns to stay fixed, one to the left, one to the right…

    • I don’t seem to be able to make wide tables work properly in Safari (5.1.7). The vertical scroll bar does not appear and I cannot limit the height of the table.

      Thanks in advance…..

  15. Hi,

    Looks good, but when we scroll up, the sticky headers has jerk while scroll.

  16. Love the script. Couple of queries.

    Looking at your second wide demo. How do I increase the width of the table container ?
    Also my header only sticks if the main page is scrolled down. Scrolling down in the container doesn’t seem to work by itself.

    Any ideas ?

  17. Hi, if I have a nav at the top of my website, how do I offset the column header row so that it doesn’t get covered by the nav? Cheers

  18. Is it possible to disable the repositioning animation? I don’t need to to bounce in, I just need it to stick.

    • I guess you’ve figured out… by the way, in css comment/modify/delete transition attribute and here you go, just stick.

      .sticky-wrap .sticky-thead,
      .sticky-wrap .sticky-col,
      .sticky-wrap .sticky-intersect {
      opacity: 0;
      position: absolute;
      top: 0;
      left: 0;
      /*transition: all .125s ease-in-out;*/ /**/
      z-index: 50;
      width: auto; /* Prevent table from stretching to full size */

  19. If the data in the cells is wider than the column header, the floating headers (that appear as you scroll down) don’t get resized properly in all browsers. To do this you need to physically insert an element into the cell to take up space and force the resize. For this I used a div tag and inserted the following code into your RenderStickyTable function:

    $t.find(‘thead th’).each(function (i) {

    Works a charm now. Thank you. Great example.

    • Hi Nigel,

      Could you elaborate a little more with your description of what you did with div tags and not sure what you meant by where you inserted your jquery

      $t.find(‘thead th’).each(function (i) {

      I might be looking to do what you just described and need a little more detail.

      Thanks 🙂

  20. Hi,
    thank you for sharing this code. I faced issue with sticky column when I have border over cells (also different behaviour across browsers). Here is how I fixed:
    //Table Border
    var cellBorderWidth = function () {
    var borderWidth = parseInt($t.find(‘tbody td’).css(‘borderWidth’));
    if (!isNaN(borderWidth))
    if ($.browser.webkit) {
    return parseInt(borderWidth);
    } else {
    return parseInt(borderWidth) – 1;
    return 0;

    and distracted border from height.


  21. Hi and sorry => where and how do i set the height of the table in wide tables sticky header and column example ? I want the table to fully fill a frame-site …


  22. Pardon the self promotion: SexyTables is my own solution to the same problem. My script uses a slightly different approach and allows click and drag for scrolling.

    Hope you like it.

  23. Great work! Question, should hovers work on the table header rows and columns before the user scrolls? The hover seems to work after the sticky header appears on scroll but not before. Maybe the generated header table is overlapping original one, preventing hover affects? Is there anyway around this?

  24. Hi,

    how to set every column for 20px width by wide table, please?

    I tried

    td, th { padding: 0.75em 1.5em; text-align: left; width: 20px;

    but that did not work.


  25. Thanx for great work
    any way to avoid throttle and debounce?
    i am using the script in java GWT application, and for some reason it doesnt like how throttle function is defined in js, so it doesnt work

  26. If I have a page with multiple tables each of which I want to have sticky headers, do I add the three entries after each table or can I put it in once to be applied for the whole page?

    I’m running into a problem with a page where, as I scroll down the page and hit my first table with sticky headers, the screen fills with repeating sticky headers.

    What do I do?

    If you reply to this message, please reply to my email address ( as I may not be checking here often.


  27. Great work! What if we want to add it for Jquery Datatable. It works perfectly but offset of sticky header won’t work accurate.


  28. I don’t seem to be able to make wide tables work properly in Safari (5.1.7). The vertical scroll bar does not appear and I cannot limit the height of the table.
    Thanks in advance…..

  29. I used this on a table populated via an ajax request, when it starts sometimes returns zero rows at the begin then i change the parameter on the request and have some results… but the sticky header doest update and leaves me just a stiky blank row, does anyone know what to do??

  30. I think the correct solution to this problem is to create a copy of the header and use “position: fixed” to align it to the top of the scrollable area when the original header has been scrolled off the screen. And, of course, the copy of the header needs to go away if the user scrolls past the end of the table, or if the full original header is in view.

    Here’s a jQuery plugin that does that effectively:

    Thanks to an awesome contributor, it even handles sticking the header to the top of a scrollable area, rather than only allowing scrolling of the full window. Whoever contributed that tidbit deserves free cookies for life.

  31. First of All, thank you for the solution (Sticky Table), it helped me a lot. And It’s the only one I found re-sizable.
    But there was a problem when i put the table in tabs. The closed tabs rendered when you open it so, something in the table not rendered yet (specially in the header), all width = 0, and displayed with offset to the left.
    I solved it with (display:block) to the hidden tab and (position:absolute) and (left:-99999), to render the tab first when DOM ready.

  32. Hi Terry this is a great piece of code.

    I’m using it for my website but having a tiny problem.

    I’ve got separate tables which load up in different tabs

    I would like each one of them to be a sticky table but only the first one I load up works and the following tabs don’t

    Any idea how to fix this

  33. Terry, not sure if you’re still responding to such an old post, but I hope you can answer this question, if it’s not too much trouble.

    I love this approach you’ve created and want to try to implement it. The only difference in my table is that I have two header rows. I was able to get two rows to work horizontally, but not vertically. Only the top-left th is designated as an “intersect” cell. How can I get the first two cells (rows) in first column to scroll vertically (not just the top cell in top row)?