Don’t Mess With Tables – Pure CSS Fixed-Header Left-Aligned Tables

Last modified date

table with one leg melting metal

See the Demo Page for examples and view the source for markup and CSS.

Update 9/3/13: I just added Zupa’s suggestion to the demo.

I am working on a project with a requirement for fixed-headers tables (the headers don’t scroll but the body does) which expand to fit the width of the page. We were using a JavaScript plugin which had to be called every time a page was loaded or resized. Last week one of the tables needed to have a second table within it, and I looked into CSS alternatives without JavaScript.

If you’ve ever tried to keep a table header fixed while letting the body scroll, you will find out that many solutions take the “stretch” out of the width of the table.  Tables are special and different from other HTML elements.  Browsers look at the table’s contents and then calculate from the inside out.  After much frustration, I decided not to mess with tables.

My solution is to mess with divs inside of tables. I took the divs inside the th and gave them “position: absolute; top: 0;“.  That pulls the div straight up to the top of the outer container and out of the flow of the table.  The rest of the table is a normal table and continues happily along.  The header divs retain the top left coordinate from the table, and you can even resize the page and see them shift with the columns. Unfortunately, the header divs completely loose the width they got from the th container.

My solution is a table with a scrolling body but fixed header, no JavaScript, and variable width and height.  Should you use this method? It all depends on the style of your table headers.

CONS:

  • All the problems are in the header styles.
  • No right or center aligned headers (unless you do something to set column width and hack the headers).
  • No using the column width in the style.
  • If text in a header is longer then the content in that  column it might look clipped. This is fixed by setting a min-width on the tds or content in that column.
  • The height of the table headers has to be a fixed height.
  • Horizontal scroll bars or really long non-breaking content breaks it (as well as most other methods of making a fixed-header table). (See full post.)

PROS:

  • Other then the table headers, the rest of the table is totally normal and without any hacks.
  • The table has flexible width and height.
  • No JavaScript needed, you can add JavaScript for other stuff if you like.
  • The browser will take care of everything for you on page load and resize.

SUPPORT:

  • It works in IE7+, FF, Safari, and Chrome, and Opera.
  • Some td or content in the tds need a minimum width if the header is wider then the column content.
  • IE6 has some bugs.

The th-inner divs can be styled within themselves, they can be given a fixed width, padding, margin, height, etc.  They just can’t depend on the width of the th, they use the outer container to calculate their width (which isn’t particularly helpful).  There seem to be some bugs in calculation the widths of spans within the divs as well.

I have styled a table using the jQuery plugin Table Sorter written by Christian Bach. (Please see his site for documentation. )  I used my method to keep the headers fixed but styled them to show the sorting.

75 Responses

  1. Awesome technique Miriam!
    Been looking for something like this for ages: a lot of fixed-header tables exist, but they all have fixed column widths or use JS. This even works in IE7!

        • Go to the demo page and inspect it. You can use “view source” or use developer tools depending on which browser you’re using.

          • its easy to get the code

            Pure CSS Fixed Header Variable Width Table

            $(function() {
            $("#tablesorter-demo").tablesorter({sortList:[[0,0],[2,1]], widgets: ['zebra']});
            $("#options").tablesorter({sortList: [[0,0]], headers: { 3:{sorter: false}, 4:{sorter: false}}});
            });

            body {
            padding: 20px;
            background-color: #FFF7DC;
            color: black;
            min-width: 530px;
            }
            h2, h3 {
            width: 100%;
            text-align: center;
            margin: 1.5em 0 .5em 0;
            }
            p {
            width: 50%;
            margin: 10px auto;
            }

            a {
            color: black;
            text-decoration: underline;
            }
            a:hover {
            text-decoration: none;
            background-color: #D5ECFF;
            }
            td {
            border-bottom: 1px solid #ccc;
            padding: 5px;
            text-align: left; /* IE */
            }
            td + td {
            border-left: 1px solid #ccc;
            }
            th {
            padding: 0 5px;
            text-align: left; /* IE */
            }
            .header-background {
            border-bottom: 1px solid black;
            }

            /* above this is decorative, not part of the test */

            .fixed-table-container {
            width: 50%;
            height: 200px;
            border: 1px solid black;
            margin: 10px auto;
            background-color: white;
            /* above is decorative or flexible */
            position: relative; /* could be absolute or relative */
            padding-top: 30px; /* height of header */
            }

            .fixed-table-container-inner {
            overflow-x: hidden;
            overflow-y: auto;
            height: 100%;
            }

            .header-background {
            background-color: #D5ECFF;
            height: 30px; /* height of header */
            position: absolute;
            top: 0;
            right: 0;
            left: 0;
            }

            table {
            background-color: white;
            width: 100%;
            overflow-x: hidden;
            overflow-y: auto;
            }

            .th-inner {
            position: absolute;
            top: 0;
            line-height: 30px; /* height of header */
            text-align: left;
            border-left: 1px solid black;
            padding-left: 5px;
            margin-left: -5px;
            }
            .first .th-inner {
            border-left: none;
            padding-left: 6px;
            }

            /* extra-wrap */

            .extrawrap th {
            text-align: center;
            }

            .extra-wrap {
            width: 100%;
            }

            /* Zupa styles for centered headers */

            .zupa div.zupa1 {
            margin: 0 auto !important;
            width: 0 !important;
            }

            .zupa div.th-inner {
            width: 100%;
            margin-left: -50%;
            text-align: center;
            border: none;
            }

            /* for hidden header hack to calculate widths of dynamic content */

            .hidden-head {
            min-width: 530px; /* enough width to show all header text, or bad things happen */
            }

            .hidden-header .th-inner {
            position: static;
            overflow-y: hidden;
            height: 0;
            white-space: nowrap;
            padding-right: 5px;
            }

            /* for complex headers */

            .complex.fixed-table-container {
            padding-top: 60px; /* height of header */
            overflow-x: hidden; /* for border */
            }

            .complex .header-background {
            height: 60px;
            }

            .complex-top .th-inner {
            border-bottom: 1px solid black;
            width: 100%
            }

            .complex-bottom .th-inner {
            top: 30px;
            width: 100%
            }

            .complex-top .third .th-inner { /* double row cell */
            height: 60px;
            border-bottom: none;
            background-color: #D5ECFF;
            }

            /* for tableSorter headers */

            .fixed-table-container.sort-decoration {
            overflow-x: hidden;
            min-width: 530px; /* enough width to show arrows */
            }
            .sort-decoration .th-inner {
            width: 100%;
            }
            .header .th-inner {
            background-color: #D5ECFF;
            }
            .headerSortUp .th-inner, .headerSortDown .th-inner {
            background-color: #5DDFFD;
            }
            span.sortArrow {
            background: url(icons/bg.gif) 0 4px no-repeat transparent;
            padding: 1px 10px;
            line-height: 30px;
            }
            .headerSortUp span.sortArrow {
            background: url(icons/asc.gif) 0 7px no-repeat transparent;
            }
            .headerSortDown span.sortArrow {
            background: url(icons/desc.gif) 0 7px no-repeat transparent;
            }

            (note from Miriam – I had to edit Santosh’s comment because it was very long and the line breaks and HTML tags were removed)

  2. >> If text in a header is longer then the content in that column it might look
    >> clipped.

    Another solution is to use the following class:
    .th_hidden {
    overflow-y: hidden;
    height: 0px;
    }
    Just repeat the header twice – with “th-inner” class and then with “th-hidden” and replace spaces in the second header with underscores.

    • Thanks Victor! I added that to the demo page but instead of replacing spaces with underscores I added “white-space: nowrap;” to the CSS. I also added a little padding-right.

      One caveat – all these solutions break if the combined header text is too wide to fit the width of the table without wrapping. So if these headers have lengthy mystery content then watch out! (Also, I haven’t tested this in IE yet.)

  3. This was exactly what I needed for a project I’m working on at the moment, excellent approach and really elegant solution to a common problem – thanks so much for sharing it! 🙂

  4. This technique works great, except for one thing. It doesn’t work with a horizontal scroll bar, it only has a vertical scroll bar. Is it possible to have a pure css locked header solution with horizontal and vertical scroll bars on the table?

    • The link appears to be to normal CSS for the appearance of the table. What this post is about is fixed-header tables where the table and it’s contents are of variable width but the height is limited.

    • The way this technique works the table headers don’t retain the width from the table. Because of this, they can’t be center aligned normally. This technique was intended for tables of variable width. If you have a table with a set width, or you know the width of the columns, you can style divs in the ths to those widths and center the text within them.

  5. Hey Miriam,

    2013 and this technique is still helping people.
    Big thanks. The most elegant solution I found on the web and really performant too !
    Kudos for replying to every comment as well.

    Regards,
    L.

  6. Miriam, thank you so much! This is awesome. You saved my ass.

    I needed an arrow on the right side of the header cells for a dropdown, similar to your tablesorter example. I figured by double wrapping the header content you can align one of them left the other right, thus give the impression of having a same width cell. This can be solved many ways, here is an example:

    Header

    (I realize one would use different classes instead, just wanted to use minimum change.)

    • Hm, the HTML example code was cut out. Trying with brackets instead 😛

      [th ..]
      [div]
      [div class=”th-inner”]
      [span]Header[/span]
      [/div]
      [/div]
      [div style=”direction:rtl; background:transparent; width:auto;”]
      [div class=”th-inner”]
      [span class=”sortArrow”][/span]
      [/div]
      [/div]
      [/th]

  7. Just figured out one can actually center the text by double-wrapping the header contents. The outer div (in the TH) must be centered, so the inner div will have its left position starting there, in the center.
    The you have to set its margin to -50%, so it will go from -50% to +50% relative to the center.
    Then you have the center the text.

    Note: this either won’t make your cell have the same with.
    Note2: you should not set any border or background-color this way
    Note3: you can combine left,center & right positioned methods..

    [th ..]
    [div style=”margin:0 auto; width:0;”]
    [div class=”th-inner” style=”width:100%; margin-left:-50%; text-align:center;”]
    [span]Header text[/span]
    [/div]
    [/th]
    [/div]

    • Thank you Zupa! I have added this to to demo page. I have also added a variation with hidden headers, because someone else requested it. These center-aligned styles break in IE7.

  8. Actually, there’s a better way to center/right-align the text that doesn’t require a 100% width div.

    Simply the following:

    [th style=”text-align: right”]
    [span style=”line-height: 0px; height: 0px; font-size: 0px”] [/span]
    [span style=”position: absolute; top: 0px; etc.”]HeaderText[/span]
    [span style=”visibility: hidden; line-height: 0px; height: 0px”]HeaderText[/span]
    [/th]

    You essentially have a no width non-breaking space before your absolutely positioned header, which moves it into the text alignment, then you have the actual header again inline but invisible and no height to correctly align the text you pull out.

    This has the added benefit of the column widths always being enough for your headers.

    • Adam, I tried but couldn’t get this to work. The right-align didn’t work consitently and there was a gap before the table body which is caused by the space that the 3rd span takes up. If the 3rd span doesn’t take up space, it doesn’t work to position the spans above it but but if it does take up space there is a gap. The lines of the table body start after the gap. If your technique were going to be used in modern browsers, the first span could be replaced by a CSS “span:before { …. } instead of an HTML span.

  9. I already tried dozens of scroll tables, none worked for what I want to do: I would like the table to be 50% of the windows height, instead of a fixed height.

    I took your code, set html, body { height: 100% } and changed .fixed-table-container { height: 50% } and it worked immediately. wow, impressive.

  10. Hello Miriam,

    I have thrown the code of your first fixed-header-table into codepen: http://codepen.io/helloworld/pen/qHDFB

    Can you tell me why the vertical scrollbar is not on the table body but on the whole document body? That way the header scrolls with the table and is not fixed anymore.

    Thanks for having a look at the pen.

    • You were missing the div class=”fixed-table-container” at the top. It looks like you had the closing div for that but not the starting div. I think I saved your pen with the addition, but I’m not sure since I didn’t login.

  11. Hello Miriam,

    thank for the tip before. I would like now to have a fixed table header with a 100%-height table not 200px. When I set height:100%; then the header is not fixed anymore… althouth my setarent div has no overflow: auto set but your table-tag has overflow-y:auto set I expected the table to stay fixed and have the scrollbars on the table. Do you have an idea?

    • Hi Lisa,
      Set the height however you want on the .fixed-table-container, not the table itself. (In some cases if you have a set height container and set the table height to 100% and the table doesn’t have enough content to fill the height, the rows will get taller to fill in the space.)

    • Hello Lisa, I am having a similar problem as you have, the content of my tables are generated dynamically from a db, so the height of the tables keeps changing some times the table does not have any data. What I am trying to do is if the height of the table is longer than the page, then the header gets fixed when scrolling else the height of the table just stays as it is, I changed the height of fixed-table-container from 200px to 100% but it is not working, any ideas? Thanks Vic

      • I just did a quick try in firebug on Firefox, so this isn’t tested but it seemed to work.

        On both the .fixed-table-container and .fixed-table-container-inner, remove the height completely. On .fixed-table-container-inner add max-height to what you want.

        So, to write it out
        .fixed-table-container {
        /*height: remove it */
        }
        .fixed-table-container-inner {
        /*height: 100% remove it */
        max-height: 200px; /* or whatever*/
        }

  12. Everyone, sorry I haven’t been keeping up with this in a timely way. I updated the demo page to add Zupa’s suggestion and changed the doctype to 5.

  13. Hi Miriam!
    What can i say besides: congratulations for this brilliant hack!
    I’m not a css3 noob but i just didn’t think about wrapping the header cells with a div and, basically, taking them out of the table.
    It’s so true that you never stop learning 😀

  14. thanks a lot for that solution, but i cannot make it works with a calculated width of the container. relative works, but fixed not and do not understand why :

    Adding some thing like that breaks all :

    $(window.resize(function() {
    $(‘.fixed-table-container’).width($(window).width()-140);
    });

    Any idea ?

    • I think your JavaScript is broken. I got it to work with this (the line-breaks are not being formatted correctly, sorry!)
      $(window).resize(function() {
      var changedwidth = $(window).width()-140;
      $(‘.fixed-table-container’).width(changedwidth);
      });

      (You were missing a “)” around “window” and you had curly single quotes instead of regular ticks.)

      I don’t know what you are trying to do, but if all you want is to put a 70px side margin around the .fixed-table-container, you should use CSS.
      Change “width” to “margin”. Where I have
      .fixed-table-container {
      /*width: 50%;*/
      margin: 70px;

  15. i have about 200 rows and cannot figure out how to scroll the table back to the top with a link. can you help me out? I have tried to add a row ( ) in the tbody but that did not work.

    thank you for a great solution, exactly what I needed.

  16. hi,
    its really looking nice ,
    but where is your code ?
    i dont see any links to see your code .
    or firebug should be used for it?

  17. hi,
    my table contain’s textbox or dropdown in some td.
    and as could see your solution does not work for it
    any help is welcome .

    thanks

  18. Thanks for your excellent work, saved me a ton of time. BTW, an easy way to control the column widths: I created a row which is the first row in the table with height:0px, each cell in that row has it’s min-width value set. This also required me to adjust the .fixed-table-container padding to absorb the first row so there would be no gap.

  19. I have used your complex sample, what i do same like you top one th row and bottom one th row , some of the th div are overlapping with others. If td text is less then th div header text , then th div header overlapping with near div.

    Please advise.

    • Hello Jameel,
      DId you ever get this resolved? I posted a comment last night and it sounds like a very similar issue. How did you work around the overlapping divs?

  20. The combination of the fixed headers along with the TableSorter makes for a very sleek table! Appreciate all of your work on this!

    I’m having an issue where my header text is being covered and the sorter arrows aren’t being shown. If the text in the td is wider than the th it’s fine, otherwise the th gets covered. Can you think of anything that might cause this? Thanks in advance!

  21. Great job! I’ve been trying to combine the Center Aligned (Zupa style) with hidden header with the TableSorter. Is this possible? I’m not having any luck. Thanks.

  22. We would like to use this technique and some of the css on the demo page in an open source angularjs directive for creating tables with fixed headers. Are there any licensing restrictions and how would you like to be credited? This will be in github.

    Thanks,

    Steve and Lance

  23. Hey there, I want to say that I think this is great and it has been working perfectly for me so far. I am wondering if you have ever considered adding a fixed footer as well, using the tag. I think it would be useful but I am not sure how to accomplish it. I will try and let you know how it goes, just wondering if you have ever given it any thought. Thanks!

    • I had not thought of using a fixed footer as well as a fixed header. What would the use case be for it? I figure the fixed header plus restricting the table height means the header is always in view with the rest of the table. If you can see the header at all times, why would you use an additional footer?

  24. Hi,

    I want center (text-align:center) my headings in this fixed header table. I tried it but its not working. Please help me to fix this issue.

  25. Hi..

    Thanks a lot for saving me hours of css-html digging. Excellent work, just a few style changes — none required, just for fancier look — and i was done..

  26. Is there any way to use center aligned table headers(Zupa style or maybe something else) + the jQuery tablesorter plugin?

  27. I like this a lot. Unfortunately, it doesn’t seem to be compatible with momentum scrolling. Adding -webkit-overflow-scrolling: touch to .fixed-table-container-inner causes the headings to disappear on iOS.

  28. Miriam:
    Your table solution is nice, but I notice that it cannot be printed (the header overrides the first row), Have you had any ideas how to solve it?
    This problem only happens for IE 7 or if you use IE11 compatible mode

    • I suggest using a print media query and setting the parts back to their default. I don’t know your exact code but something along the lines of

      @media print {
      .fixed-table-container {
      height: auto;
      padding-top: 0;
      overflow: visible;
      width: auto; /* or 100% */
      }

      .header-background {
      display: none;
      }
      th {
      background-color: #d5ecff;
      }
      .th-inner {
      position: static;
      }
      }

      You’re setting the code back to “normal” and doing the sorts of things you would normally do for printing tables or printing in general.

  29. The only problem with this solution is that if you resize the page the headers will overflow into the next column. (Or it will decide some columns need more space than others causing the headers to overflow on top of each other)

  30. I tried your code but it does not work with the alignment of the rows after the header. See if I am doing something wrong as I need something like this for a project I am working on.
    Here is the code:

    div.tableContainer {
    clear: both;
    border: 1px solid #963;
    height: 285px; /* html>body tbody.scrollContent height plus 23px for the header */
    overflow: auto;
    width: 100% /* Remember to leave 16px for the scrollbar! */
    }

    html>body tbody.scrollContent {
    display: block;
    height: 262px;
    overflow: auto;
    width: 100%
    }

    html>body thead.fixedHeader tr {
    display: block
    }

    html>body thead.fixedHeader th { /* TH 1 section*/
    width: 75px
    }

    html>body thead.fixedHeader th + th { /* TH 2 plot */
    width: 75px
    }

    html>body thead.fixedHeader th + th + th { /* TH 3 status */
    width: 75px
    }

    html>body thead.fixedHeader th + th + th + th { /* TH 4 sharable */
    width: 75px
    }

    html>body thead.fixedHeader th + th + th + th + th { /* TH 5 Occup F Name */
    width: 150px
    }

    html>body thead.fixedHeader th + th + th + th + th+ th{ /* TH 6 Occup L name */
    width: 200px
    }

    html>body thead.fixedHeader th + th + th + th + th + th + th { /* TH 7 Owner F Name */
    width: 150px
    }

    html>body thead.fixedHeader th + th + th + th + th + th + th + th{ /* TH 8 Owner L Name */
    width: 200px
    }

    html>body thead.fixedHeader th + th + th + th + th + th + th + th + th{ /* TH 9 Reserved F Name */
    width: 150px
    }

    html>body thead.fixedHeader th + th + th + th + th + th + th + th +th +th{ /* TH 10 Reserved L Name*/
    width: 200px
    }

    html>body thead.fixedHeader th + th + th + th + th + th + th + th + th + th + th{ /* TH 11 Veteran */
    width: 50px
    }

    html>body thead.fixedHeader th + th + th + th + th + th + th + th + th + th + th + th{ /* TH 12 Knight */
    width: 50px
    }

    html>body thead.fixedHeader th + th + th + th + th + th + th + th + th + th + th + th + th{ /* TH 13 View and Edit */
    width: 100px
    }
    html>body tbody.scrollContent td { /* TD 1 Section*/
    width: 75px
    }

    html>body thead.scrollContent td + td { /* TD 2 Plot */
    width: 75px
    }

    html>body thead.scrollContent td + td + td { /* TD 3 status */
    width: 75px
    }

    html>body thead.scrollContent td + td + td + td { /* TD 4 Sharable */
    width: 75px
    }

    html>body thead.scrollContent td + td + td + td + td { /* TD 5 Occup F Name */
    width: 150px
    }

    html>body thead.scrollContent td + td + td + td + td + td { /* TD 6 Occup L. Name */
    width: 200px
    }

    html>body thead.scrollContent td + td + td + td + td + td + td { /* TD 7 Owner F. Name */
    width: 150px
    }

    html>body thead.scrollContent td + td + td + td + td + td + td + td { /* TD 8 Owner L Name */
    width: 200px
    }

    html>body thead.scrollContent td + td + td + td + td + td + td + td + td { /* TD 9 Reserved F Name */
    width: 150x
    }

    html>body thead.scrollContent td + td + td + td + td + td + td + td + td + td { /* TD 10 Reserved L. Name */
    width: 200px
    }

    html>body thead.scrollContent td + td + td + td + td + td + td + td + td + td + td { /* TD 11 Veteran */
    width: 50px
    }

    html>body thead.scrollContent td + td + td + td + td + td + td + td + td + td + td + td { /* TD 12 Knight */
    width: 50px
    }

    html>body thead.scrollContent td + td + td + td + td + td + td + td + td + td + td + td + td { /* TD 13 Edit View */
    width: 100px
    }

    Section
    Plot
    Plot Status
    Sharable
    Occup F.Name
    Occup L.Name
    Owner F.Name
    Owner L.Name
    Reserved F.Name
    Reserved L.Name
    Veteran
    Knight
    View/Edit

    A
    1
    Occupied
    Yes
    Joesph
    Smithsonian
    Joethe great
    SmithandWesson

    Yes
    No
    Edit/View…

  31. Is there any way to print the scrollable table – when I tried it only prints the page in view, not the entire table which may have 100 rows.

    • Hi Jack,
      I’m not looking at the specifics right now, but generally the way is to have specific style rules for print and to change the position there. The code below seemed to get


      @media print {
      .fixed-table-container {
      height: auto; /* or don't give the height at all */
      }
      .header-background {
      display: none;
      }
      .th-inner {
      position: static; /* or no position at all */
      }
      }

      Additionally, you can add other styles just for printing in the media query. I would look for position: and change it to static, width and height to auto or 100%, and overflow to auto.

      I hope that helps!

  32. I’m struggling to understand the rules behind the construction of the “complex header” solution – I iave a table with more columns (and row/column spans) – it’d be great if there were some more examples of other complex headers (or an explanation somewhere of how the specific construction works)

Leave a Reply

Your email address will not be published. Required fields are marked *

Post comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.