Table fixed header and scrollable body

986,589

Solution 1

Here is the working solution:

table {
    width: 100%;
}

thead, tbody, tr, td, th { display: block; }

tr:after {
    content: ' ';
    display: block;
    visibility: hidden;
    clear: both;
}

thead th {
    height: 30px;

    /*text-align: left;*/
}

tbody {
    height: 120px;
    overflow-y: auto;
}

thead {
    /* fallback */
}


tbody td, thead th {
    width: 19.2%;
    float: left;
}
<link href="http://netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet"/>

<table class="table table-striped">
    <thead>
        <tr>
            <th>Make</th>
            <th>Model</th>
            <th>Color</th>
            <th>Year</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td class="filterable-cell">Ford</td>
            <td class="filterable-cell">Escort</td>
            <td class="filterable-cell">Blue</td>
            <td class="filterable-cell">2000</td>
        </tr>
        <tr>
            <td class="filterable-cell">Ford</td>
            <td class="filterable-cell">Escort</td>
            <td class="filterable-cell">Blue</td>
            <td class="filterable-cell">2000</td>
        </tr>
        <tr>
            <td class="filterable-cell">Ford</td>
            <td class="filterable-cell">Escort</td>
            <td class="filterable-cell">Blue</td>
            <td class="filterable-cell">2000</td>
        </tr>
        <tr>
            <td class="filterable-cell">Ford</td>
            <td class="filterable-cell">Escort</td>
            <td class="filterable-cell">Blue</td>
            <td class="filterable-cell">2000</td>
        </tr>
    </tbody>
</table>

Link to jsfiddle

Solution 2

Fixed table head - CSS-only

Simply position: sticky; top: 0; your th elements. (Chrome, FF, Edge)

.tableFixHead          { overflow: auto; height: 100px; }
.tableFixHead thead th { position: sticky; top: 0; z-index: 1; }

/* Just common table stuff. Really. */
table  { border-collapse: collapse; width: 100%; }
th, td { padding: 8px 16px; }
th     { background:#eee; }
<div class="tableFixHead">
  <table>
    <thead>
      <tr><th>TH 1</th><th>TH 2</th></tr>
    </thead>
    <tbody>
      <tr><td>A1</td><td>A2</td></tr>
      <tr><td>B1</td><td>B2</td></tr>
      <tr><td>C1</td><td>C2</td></tr>
      <tr><td>D1</td><td>D2</td></tr>
      <tr><td>E1</td><td>E2</td></tr>
    </tbody>
  </table>
</div>

For both sticky vertical TH and horizontal TH columns (inside TBODY):

.tableFixHead          { overflow: auto; height: 100px; width: 240px; }
.tableFixHead thead th { position: sticky; top: 0; z-index: 1; }
.tableFixHead tbody th { position: sticky; left: 0; }

.tableFixHead          { overflow: auto; height: 100px; width: 240px; }
.tableFixHead thead th { position: sticky; top: 0; z-index: 1; }
.tableFixHead tbody th { position: sticky; left: 0; }

/* Just common table stuff. Really. */
table  { border-collapse: collapse; width: 100%; }
th, td { padding: 8px 16px; white-space: nowrap; }
th     { background:#eee; }
<div class="tableFixHead">
  <table>
    <thead>
      <tr><th></th><th>TH 1</th><th>TH 2</th></tr>
    </thead>
    <tbody>
            <tr><th>Foo</th><td>Some long text lorem ipsum</td><td>Dolor sit amet</td></tr>
            <tr><th>Bar</th><td>B1</td><td>B2</td></tr>
            <tr><th>Baz</th><td>C1</td><td>C2</td></tr>
            <tr><th>Fuz</th><td>D1</td><td>D2</td></tr>
            <tr><th>Zup</th><td>E1</td><td>E2</td></tr>
    </tbody>
  </table>
</div>

TH borders problem fix

Since border cannot be painted properly on a translated TH element, to recreate and render "borders" use the box-shadow property:

/* Borders (if you need them) */
.tableFixHead,
.tableFixHead td {
  box-shadow: inset 1px -1px #000;
}
.tableFixHead th {
  box-shadow: inset 1px 1px #000, 0 1px #000;
}

.tableFixHead          { overflow: auto; height: 100px; }
.tableFixHead thead th { position: sticky; top: 0; z-index: 1; }

/* Just common table stuff. Really. */
table  { border-collapse: collapse; width: 100%; }
th, td { padding: 8px 16px; }
th     { background:#eee; }

/* Borders (if you need them) */
.tableFixHead,
.tableFixHead td {
  box-shadow: inset 1px -1px #000;
}
.tableFixHead th {
  box-shadow: inset 1px 1px #000, 0 1px #000;
}
<div class="tableFixHead">
  <table>
    <thead>
      <tr><th>TH 1</th><th>TH 2</th></tr>
    </thead>
    <tbody>
      <tr><td>A1</td><td>A2</td></tr>
      <tr><td>B1</td><td>B2</td></tr>
      <tr><td>C1</td><td>C2</td></tr>
      <tr><td>D1</td><td>D2</td></tr>
      <tr><td>E1</td><td>E2</td></tr>
    </tbody>
  </table>
</div>

Fixed table head - using JS. (IE)

You can use a bit of JS and translateY the th elements

jQuery example

var $th = $('.tableFixHead').find('thead th')
$('.tableFixHead').on('scroll', function() {
  $th.css('transform', 'translateY('+ this.scrollTop +'px)');
});
.tableFixHead { overflow-y: auto; height: 100px; }

/* Just common table stuff. */
table  { border-collapse: collapse; width: 100%; }
th, td { padding: 8px 16px; }
th     { background:#eee; }
<div class="tableFixHead">
  <table>
    <thead>
      <tr><th>TH 1</th><th>TH 2</th></tr>
    </thead>
    <tbody>
      <tr><td>A1</td><td>A2</td></tr>
      <tr><td>B1</td><td>B2</td></tr>
      <tr><td>C1</td><td>C2</td></tr>
      <tr><td>D1</td><td>D2</td></tr>
      <tr><td>E1</td><td>E2</td></tr>
    </tbody>
  </table>
</div>

<script src="https://code.jquery.com/jquery-3.1.0.js"></script>

Or plain ES6 if you prefer (no jQuery required):

// Fix table head
function tableFixHead (e) {
    const el = e.target,
          sT = el.scrollTop;
    el.querySelectorAll("thead th").forEach(th => 
      th.style.transform = `translateY(${sT}px)`
    );
}
document.querySelectorAll(".tableFixHead").forEach(el => 
    el.addEventListener("scroll", tableFixHead)
);

Solution 3

Likely you'll get multiple tables on one page, therefore you need CSS classes. Please find a modified @giulio's solution for that.

Just declare it in table:

<table class="table table-striped header-fixed"></table>

CSS

.header-fixed {
    width: 100% 
}

.header-fixed > thead,
.header-fixed > tbody,
.header-fixed > thead > tr,
.header-fixed > tbody > tr,
.header-fixed > thead > tr > th,
.header-fixed > tbody > tr > td {
    display: block;
}

.header-fixed > tbody > tr:after,
.header-fixed > thead > tr:after {
    content: ' ';
    display: block;
    visibility: hidden;
    clear: both;
}

.header-fixed > tbody {
    overflow-y: auto;
    height: 150px;
}

.header-fixed > tbody > tr > td,
.header-fixed > thead > tr > th {
    width: 20%;
    float: left;
}

Be aware that current implementation suits five columns only. If you need a different number, change the width parameter from 20% to *100% / number_of_columns*.

Solution 4

Update

For newer and still maintained library try jquery.floatThead (as mentioned by Bob Jordan in the comment) instead.

Old Answer

This is a very old answer, the library mentioned below no longer maintained.

I am using StickyTableHeaders on GitHub and it works like charm!

I had to add this css to make the header not transparent though.

table#stickyHeader thead {
  border-top: none;
  border-bottom: none;
  background-color: #FFF;
}

Solution 5

Don't need the wrap it in a div...

CSS:

tr {
width: 100%;
display: inline-table;
table-layout: fixed;
}

table{
 height:300px;              // <-- Select the height of the table
 display: block;
}
tbody{
  overflow-y: scroll;      
  height: 200px;            //  <-- Select the height of the body
  width: 100%;
  position: absolute;
}

Bootply : https://www.codeply.com/p/nkaQFc713a

Share:
986,589
giulio
Author by

giulio

MSc Student of Computer Science at ETH Zürich

Updated on August 02, 2022

Comments

  • giulio
    giulio almost 2 years

    I am trying to make a table with fixed header and a scrollable content using the bootstrap 3 table. Unfortunately the solutions I have found does not work with bootstrap or mess up the style.

    Here there is a simple bootstrap table, but for some reason to me unknown the height of the tbody is not 10px.

    height: 10px !important; overflow: scroll;
    

    Example:

    <link rel="stylesheet" type="text/css" href="//netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css">
    
    <table class="table table-striped">
        <thead>
        <tr>
            <th>Make</th>
            <th>Model</th>
            <th>Color</th>
            <th>Year</th>
        </tr>
        </thead>
        <tbody style="height: 10px !important; overflow: scroll; ">
        <tr>
            <td class="filterable-cell">111 Ford</td>
            <td class="filterable-cell">Escort</td>
            <td class="filterable-cell">Blue</td>
            <td class="filterable-cell">2000</td>
        </tr>
        <tr>
            <td class="filterable-cell">Ford</td>
            <td class="filterable-cell">Escort</td>
            <td class="filterable-cell">Blue</td>
            <td class="filterable-cell">2000</td>
        </tr>
                <tr>
            <td class="filterable-cell">Ford</td>
            <td class="filterable-cell">Escort</td>
            <td class="filterable-cell">Blue</td>
            <td class="filterable-cell">2000</td>
        </tr>
         <tr>
            <td class="filterable-cell">Ford</td>
            <td class="filterable-cell">Escort</td>
            <td class="filterable-cell">Blue</td>
            <td class="filterable-cell">2000</td>
        </tr>
        </tbody>
        
    </table>
  • giulio
    giulio over 10 years
    display:block was necessary but then I also had to put float:left and a proper width for all the cell. The solution I have posted is now working.
  • T.J. Crowder
    T.J. Crowder almost 10 years
    Doesn't work in IE9 (and probably others); requires fixed-width columns (and there are much simpler, more cross-browser solutions if you're going to use fixed-width columns); breaks badly if the content exceeds the fixed width; doesn't actually line up the headers and the columns (you can see it in the fiddle linked above; it's clearer in this version that gives them a simple border) although that could probably be fixed; the scrollbar isn't quite lined up with the body of the table...
  • Apolo
    Apolo over 9 years
    add a visible border to td and th and you will see that td's width does not match th's width
  • Tmac
    Tmac about 9 years
    This looks great until you realize that the column data has to be the same width. If not then data doesn't fit under the headings.
  • Chetan
    Chetan about 9 years
    after following all the different complex solutions, I find that this is the best solution. However the vertical scroll bar at has some width(which is obvious), but due to it there is the slight mismatch alignment between headers and the rows. I have 7 columns, so I have set width to 14.28% after calculating using the mention formula
  • user1807271
    user1807271 almost 9 years
    How would I assign a certain width to each column?
  • Luke
    Luke over 8 years
    @user1807271 you can assign each column's width via JS, or create a per-column class (say "col1", "col2" and so on) with the width you need and assign class "col1" to all the cells on the first column, "col2" con the second one, etc..
  • Rob Sedgwick
    Rob Sedgwick over 8 years
    Note that the stickyTableHeaders plugin will only find html that is in the page when browser loaded it initially, it won't pick up dynamically generated content
  • Rosdi Kasim
    Rosdi Kasim over 8 years
    @RobSedgwick, I haven't this this, but it should still work. As long as you initialize stickyTableHeadres AFTER the table is generated. Meaning you can't initialize it in the head but instead initialize it right after the dynamically generated table is completed.
  • Jeffrey A. Gochin
    Jeffrey A. Gochin over 8 years
    The problem with this solution is that it requires a fixed height. That is not going to work where responsive design come in. I have been working on a solution that uses position: fixed, which solves the scrolling issue, but messes up the row widths.
  • Omar Wagih
    Omar Wagih about 8 years
    I suggest you add .header-fixed > thead > tr > th{white-space: nowrap;} as well. If headers start wrapping it messes things up
  • Matt Inamdar
    Matt Inamdar about 8 years
    Fantastic solution. Much appreciated. If anyone is having problems making the background opaque, I had to use .tableFloatingHeaderOriginal { //css }.
  • John Zabroski
    John Zabroski almost 8 years
    This solution doesn't work when individual cells overflow.
  • Ben Hoffman
    Ben Hoffman almost 8 years
    You can manually fix the td width not matching the th width by seperating out the width % declaration for the td & th and just set the th to be slightly more narrow. You will have do it custom every time but this isn't something that should be used that often.
  • Dejell
    Dejell over 7 years
    what if I don't want to have a scroll?
  • Dmitry
    Dmitry over 7 years
    tbody after setting width: 100% filled page width. Was fixed by position: relative to table
  • Mati
    Mati over 7 years
    This plugin did not work for me, as occasionally the head floats out of the table and then floats back, which makes it look like st is broken.
  • kevinc
    kevinc about 7 years
    this does not work in chrome, either the bootply or the solution.
  • M2012
    M2012 almost 7 years
    How can we adjust vertical scroll bar space. Can we do it by using css.
  • shubhamkes
    shubhamkes almost 7 years
    It worked for me but now all td is having same width. Which i dont want.
  • Washington Guedes
    Washington Guedes about 6 years
    Simply awesome. Side note: th:nth-child(2), td:nth-child(2) is equivalent to tr > :nth-child(2)
  • PirateApp
    PirateApp almost 6 years
    i want both the first column and the header to be fixed which is not possible as per the data tables compatibility chart
  • A P
    A P over 5 years
    @Roko thank you so much for providing such a clean solution, it works like charm in chrome. Unfortunately, this is not working in IE11. Is there any tweak to make it work in IE11.
  • Roko C. Buljan
    Roko C. Buljan over 5 years
    @AP for IE11 (since it does not understands sticky) you could use the second example that uses translateY (and the jQuery code - if you don't compile your ES6 js code). It'll work, but will look a bit "jumpish". But that's OK. If one wants to use outdated browsers than such person/company will never complain about choppy things - as far as he's able to see the content and have a decent interaction with your website. Interesting read ahead: nealbuerger.com/2018/01/the-end-of-life-of-internet-explorer‌​-11
  • BENARD Patrick
    BENARD Patrick about 5 years
    Just for comment, It works for me in Chrome, the table is at the middle top of the page content.... Can you provide more info ? (I'm on linux)
  • user3191192
    user3191192 almost 5 years
    Does not work when there is more than one row in the table header. When scrolling the 2nd header line jumps up and lays over the first because they both have "top:0". Tinkered around with making the entire <thead> sticky but could not make it work. Any ideas for multi-line headers (with colspans to boot)?
  • user3191192
    user3191192 almost 5 years
    Specifically I was referring to the CSS solution. The JS solution works better and both header rows stick. But they both have a minor visual flaw, at least on Chrome 74 - the header border lines are not drawn and rows that scroll behind it become a bit visible. Likewise the top table border also does not get drawn and a pixel of the scrolled table data becomes visible at the top edge of the table.
  • Roko C. Buljan
    Roko C. Buljan almost 5 years
    @user3191192 (using JS example) on desktop, Chrome 74 works great for me, only on mobile there's a small hop. Regarding TH border, yes, it cannot be drawn by the browser. See my edited answer for a solution using box-shadow.
  • user2182349
    user2182349 almost 5 years
    You can overcome the table-layout:fixed by adding a CSS class with JavaScript
  • Ivan Koshelev
    Ivan Koshelev over 4 years
    If the top solution of { position: sticky; top: 0; } somehow does not work in your code - check the overflow-x and overflow-y css properties of containing elements, especially table, thead, tr. Overflows, surprisingly both of them, must not be set. You may need to explicitly unset them. More info - stackoverflow.com/a/54806576/882936
  • ViBoNaCci
    ViBoNaCci about 4 years
    position: sticky is widely underused and a bit counter intuitive, especially since you need the top: 0 as well. But this is actually a perfect case to show how to use position sticky purposefully!
  • shao.lo
    shao.lo over 3 years
    I skipped over this one for the much higher rated answer by @Roko C. Buljan below only to find that answer didn't work when the header had multiple rows. This solution works great. Thank you so much!!!
  • blissweb
    blissweb over 3 years
    I did display:block on the tbody and thead and they align perfectly. Block is the key for the scrolling and height to work. Thanks.
  • deanwilliammills
    deanwilliammills over 3 years
    th does not align with td
  • Michael
    Michael over 3 years
    Dunno if it's because my table is in a nested grid layout or the fact that the table is really wide, but this produces the most bizarre rendering on my table - it's like each column is placed a couple pixels offset to the right so each column nearly overwrites the previous one resulting in a very malfunctioned look
  • izogfif
    izogfif over 3 years
    This doesn't work if you want to use <colgroup><col style="width: 10px;"><col style="width: 100px;"></colgroup> to manage width of columns of your table - all your settings are ignored.
  • Jan Turoň
    Jan Turoň about 3 years
    Upvoted with a note that the outer element is not needed (just set block to table display, anonymous table wrapper will be created), fixed table layout is not needed and sticky can't be set on thead and tr in today's browsers (would you add this info to your answer to save others troubles implementing it?)
  • Jason Lim Ji Chen
    Jason Lim Ji Chen about 3 years
    Had issue with this solution on the header row: The header role only capable of having not more than 4 column, and new row will summon if you're having more than 4 columns.*Need to adjust the width % to your corresponding row number*
  • ibexy
    ibexy about 3 years
    How do you add a horizontal scroll to the css only option?
  • Roko C. Buljan
    Roko C. Buljan about 3 years
    @ibexy edited my answer to account for sticky TH columns. Here's a demo: jsfiddle.net/RokoCB/863s5mt4
  • Tom Shelley
    Tom Shelley over 2 years
    got around the colgroup issue with .table-scroll th:nth-child(1), .table-scroll td:nth-child(1) { width: 3%; } for n number of columns
  • Mark
    Mark over 2 years
    Worked like a charm! Thanks. It starts floating when the header goes offscreen. Just what I needed. I did set a background color so its not mixing with the table content and a z-index:2, so its in front
  • AdityaDees
    AdityaDees about 2 years
    this not working for rowspan header @RokoC.Buljan
  • Roko C. Buljan
    Roko C. Buljan about 2 years
    It is working: jsfiddle.net/L69ho31p @AdityaDees
  • AdityaDees
    AdityaDees about 2 years
    i've post the answer about what I mean stackoverflow.com/a/71291701/7590238
  • mplungjan
    mplungjan about 2 years
    Would you mind making a minimal reproducible example using the [<>] snippet editor?
  • Alex Klaus
    Alex Klaus about 2 years
    @mplungjan, added a code snippet.
  • Steve Horvath
    Steve Horvath about 2 years
    Unlike the top solution, this requires a table height set to lower than the page and an extra inner scrollbar will also appear. Sometimes this is what you want (eg for a fixed-size inner table) but otherwise it's less than ideal.