504

How can I set for <table> 100% width and put only inside <tbody> vertical scroll for some height?

vertical scroll inside tbody

table {
    width: 100%;
    display:block;
}
thead {
    display: inline-block;
    width: 100%;
    height: 20px;
}
tbody {
    height: 200px;
    display: inline-block;
    width: 100%;
    overflow: auto;
}
  
<table>
  <thead>
    <tr>
     <th>Head 1</th>
     <th>Head 2</th>
     <th>Head 3</th>
     <th>Head 4</th>
     <th>Head 5</th>
    </tr>
  </thead>
  <tbody>
    <tr>
     <td>Content 1</td>
     <td>Content 2</td>
     <td>Content 3</td>
     <td>Content 4</td>
     <td>Content 5</td>
    </tr>
  </tbody>
</table>

I want to avoid adding some additional div, all I want is simple table like this and when I trying to change display, table-layout, position and much more things in CSS table not working good with 100% width only with fixed width in px.

msrd0
  • 7,816
  • 9
  • 47
  • 82
Marko S
  • 5,097
  • 3
  • 14
  • 11

13 Answers13

764

In order to make <tbody> element scrollable, we need to change the way it's displayed on the page i.e. using display: block; to display that as a block level element.

Since we change the display property of tbody, we should change that property for thead element as well to prevent from breaking the table layout.

So we have:

thead, tbody { display: block; }

tbody {
    height: 100px;       /* Just for the demo          */
    overflow-y: auto;    /* Trigger vertical scroll    */
    overflow-x: hidden;  /* Hide the horizontal scroll */
}

Web browsers display the thead and tbody elements as row-group (table-header-group and table-row-group) by default.

Once we change that, the inside tr elements doesn't fill the entire space of their container.

In order to fix that, we have to calculate the width of tbody columns and apply the corresponding value to the thead columns via JavaScript.

Auto Width Columns

Here is the jQuery version of above logic:

// Change the selector if needed
var $table = $('table'),
    $bodyCells = $table.find('tbody tr:first').children(),
    colWidth;

// Get the tbody columns width array
colWidth = $bodyCells.map(function() {
    return $(this).width();
}).get();

// Set the width of thead columns
$table.find('thead tr').children().each(function(i, v) {
    $(v).width(colWidth[i]);
});

And here is the output (on Windows 7 Chrome 32):

Vertical scroll inside tbody

Working demo.

Full Width Table, Relative Width Columns

As the original poster needed, we could expand the table to 100% of width of its container, and then using a relative (Percentage) width for each columns of the table.

table {
    width: 100%; /* Optional */
}

tbody td, thead th {
    width: 20%;  /* Optional */
}

Since the table has a (sort of) fluid layout, we should adjust the width of thead columns when the container resizes.

Hence we should set the columns' widths once the window is resized:

// Adjust the width of thead cells when *window* resizes
$(window).resize(function() {
    /* Same as before */
}).resize(); // Trigger the resize handler once the script runs

The output would be:

Fluid Table with vertical scroll inside tbody

Working demo.


Browser Support and Alternatives

I've tested the two above methods on Windows 7 via the new versions of major Web Browsers (including IE10+) and it worked.

However, it doesn't work properly on IE9 and below.

That's because in a table layout, all elements should follow the same structural properties.

By using display: block; for the <thead> and <tbody> elements, we've broken the table structure.

Redesign layout via JavaScript

One approach is to redesign the (entire) table layout. Using JavaScript to create a new layout on the fly and handle and/or adjust the widths/heights of the cells dynamically.

For instance, take a look at the following examples:

Nesting tables

This approach uses two nested tables with a containing div. The first table has only one cell which has a div, and the second table is placed inside that div element.

Check the Vertical scrolling tables at CSS Play.

This works on most of web browsers. We can also do the above logic dynamically via JavaScript.

Table with fixed header on scroll

Since the purpose of adding vertical scroll bar to the <tbody> is displaying the table header at the top of each row, we could position the thead element to stay fixed at the top of the screen instead.

Here is a Working Demo of this approach performed by Julien. It has a promising web browser support.

And here a pure CSS implementation by Willem Van Bockstal.


The Pure CSS Solution

Here is the old answer. Of course I've added a new method and refined the CSS declarations.

Table with Fixed Width

In this case, the table should have a fixed width (including the sum of columns' widths and the width of vertical scroll-bar).

Each column should have a specific width and the last column of thead element needs a greater width which equals to the others' width + the width of vertical scroll-bar.

Therefore, the CSS would be:

table {
    width: 716px; /* 140px * 5 column + 16px scrollbar width */
    border-spacing: 0;
}

tbody, thead tr { display: block; }

tbody {
    height: 100px;
    overflow-y: auto;
    overflow-x: hidden;
}

tbody td, thead th {
    width: 140px;
}

thead th:last-child {
    width: 156px; /* 140px + 16px scrollbar width */
}

Here is the output:

Table with Fixed Width

WORKING DEMO.

Table with 100% Width

In this approach, the table has a width of 100% and for each th and td, the value of width property should be less than 100% / number of cols.

Also, we need to reduce the width of thead as value of the width of vertical scroll-bar.

In order to do that, we need to use CSS3 calc() function, as follows:

table {
    width: 100%;
    border-spacing: 0;
}

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

thead tr {
    /* fallback */
    width: 97%;
    /* minus scroll bar width */
    width: -webkit-calc(100% - 16px);
    width:    -moz-calc(100% - 16px);
    width:         calc(100% - 16px);
}

tr:after {  /* clearing float */
    content: ' ';
    display: block;
    visibility: hidden;
    clear: both;
}

tbody {
    height: 100px;
    overflow-y: auto;
    overflow-x: hidden;
}

tbody td, thead th {
    width: 19%;  /* 19% is less than (100% / 5 cols) = 20% */
    float: left;
}

Here is the Online Demo.

Note: This approach will fail if the content of each column breaks the line, i.e. the content of each cell should be short enough.


In the following, there are two simple example of pure CSS solution which I created at the time I answered this question.

Here is the jsFiddle Demo v2.

Old version: jsFiddle Demo v1

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Hashem Qolami
  • 97,268
  • 26
  • 150
  • 164
  • 29
    With `display: block` you loose table's properties as column's width shared between thead and tbody. I know you have fixed this by setting `width: 19.2%` but in case we don't know the width of each column in advance, this won't work. – samb Jan 13 '14 at 11:31
  • 13
    This code seems to assume the body cells will always be wider than the header cells. – Adam Donahue Mar 18 '15 at 20:43
  • 5
    Not working when `height: 100%` (when you want the table to expand to the bottom of the page). – AlikElzin-kilaka Oct 22 '15 at 10:34
  • 2
    You can improve this a little more with the `box-sizing` property so that having borders of two different thicknesses doesn't throw off the column alignment. – Ricca Jul 20 '16 at 00:25
  • If you're using it in typescript, the colWidth var definition doesn't work... need to do `colWidth = $bodyCells.toArray().map(function() { return $(this).width(); });` because jquery.map return a jquery object and not a number array – S.Galarneau Jul 26 '18 at 14:44
111

In following solution, table occupies 100% of the parent container, no absolute sizes required. It's pure CSS, flex layout is used.

Here is how it looks: enter image description here

Possible disadvantages:

  • vertical scrollbar is always visible, regardless of whether it's required;
  • table layout is fixed - columns do not resize according to the content width (you still can set whatever column width you want explicitly);
  • there is one absolute size - the width of the scrollbar, which is about 0.9em for the browsers I was able to check.

HTML (shortened):

<div class="table-container">
    <table>
        <thead>
            <tr>
                <th>head1</th>
                <th>head2</th>
                <th>head3</th>
                <th>head4</th>
            </tr>
        </thead>
        <tbody>
            <tr>
                <td>content1</td>
                <td>content2</td>
                <td>content3</td>
                <td>content4</td>
            </tr>
            <tr>
                <td>content1</td>
                <td>content2</td>
                <td>content3</td>
                <td>content4</td>
            </tr>
            ...
        </tbody>
    </table>
</div>

CSS, with some decorations omitted for clarity:

.table-container {
    height: 10em;
}
table {
    display: flex;
    flex-flow: column;
    height: 100%;
    width: 100%;
}
table thead {
    /* head takes the height it requires, 
    and it's not scaled when table is resized */
    flex: 0 0 auto;
    width: calc(100% - 0.9em);
}
table tbody {
    /* body takes all the remaining available space */
    flex: 1 1 auto;
    display: block;
    overflow-y: scroll;
}
table tbody tr {
    width: 100%;
}
table thead, table tbody tr {
    display: table;
    table-layout: fixed;
}

full code on jsfiddle

Same code in LESS so you can mix it in:

.table-scrollable() {
  @scrollbar-width: 0.9em;
  display: flex;
  flex-flow: column;

  thead,
  tbody tr {
    display: table;
    table-layout: fixed;
  }

  thead {
    flex: 0 0 auto;
    width: ~"calc(100% - @{scrollbar-width})";
  }

  tbody {
    display: block;
    flex: 1 1 auto;
    overflow-y: scroll;

    tr {
      width: 100%;
    }
  }
}
tsayen
  • 2,875
  • 3
  • 18
  • 21
  • 2
    Thanks for the solution! There was a slight bug.... you set the width for thead twice, and I found that subtracting 1.05em from the width lines up a bit better: https://jsfiddle.net/xuvsncr2/170/ – TigerBear Nov 13 '15 at 23:21
  • 1
    what about the content number of character? it messes up when adding more characters or words inside de row cells – Limon Jan 05 '16 at 16:14
  • I think setting some suitable wrapping policy on `td` should help – tsayen Jan 06 '16 at 13:46
  • If you don't want table to occupy 100% of the parent container, you can try to add another container between table and parent, and set its width to whatever you want. – tsayen Mar 08 '16 at 12:40
  • Love that solution, thanks! I don't want the table to occupy 100% height (because of table border styling). This makes it be as high as required and start scrolling once it fills its container: `.table-container { display: flex; flex-direction: column; justify-content: flex-start; }` – T. Roggendorf Sep 28 '16 at 08:58
  • 2
    @tsayen How do you stop it from scrunching up and overlapping when you resize the window? – clearshot66 May 04 '17 at 20:00
  • Since the width of the scrollbar varies across browsers, I used a hidden ::after pseudo-element to render a spacer with the scrollbar turned on at the end of the header row. In order to make that work, I also used flexbox to make sure the td/th cells expanded to fill the rest of the row (the table layout engine didn't work for this). `thead tr::after { visibility: hidden; overflow-y: scroll; content: ''; } tr { display: flex; } th, td { flex: 1 0 0; } ` – Jonah Kagan Mar 02 '21 at 19:14
  • One shot and it's working but it may need size to adjust if you are using custom scrollbars like: `tbody { width: 99%; }` – EgoistDeveloper Oct 25 '22 at 20:08
56

CSS-only

for Chrome, Firefox, Edge (and other evergreen browsers)

Simply position: sticky; top: 0; your th elements:

/* Fix table head */
.tableFixHead    { overflow: auto; height: 100px; }
.tableFixHead th { position: sticky; top: 0; }

/* 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>

PS: if you need borders for TH elements th {box-shadow: 1px 1px 0 #000; border-top: 0;} will help (since the default borders are not painted correctly on scroll).

For a variant of the above that uses just a bit of JS in order to accommodate for IE11 see this answer Table fixed header and scrollable body

Roko C. Buljan
  • 196,159
  • 39
  • 305
  • 313
  • If you have borders for th elements, using "border-collapse: collapse" will stick the border to the body instead of to the header. To avoid this problem you should use "border-collapse: separate". See https://stackoverflow.com/a/53559396 – mrod Mar 25 '20 at 11:09
  • @mrod when using `separate` you're adding oil to fire. https://jsfiddle.net/RokoCB/o0zL4xyw/ and see a solution [HERE: add borders to fixed TH](https://stackoverflow.com/a/47923622/383904) - I'm welcome for suggestions OFC if I missed something. – Roko C. Buljan Mar 25 '20 at 12:41
  • 2
    This is actually the best solution to the problem. But I think it is better to make the entire header row sticky (ie. .tableFixHead thead tr) instead of the th elements, then you also 'stick' the styling of the header row. – ejectamenta Jun 07 '23 at 16:29
  • 1
    @ejectamenta it's the most obvious solution to make TR sticky indeed. Tested it and it works - at least in Chrome. If I recall well (at the time of writing) there was a bug related to making TR sticky. Cannot remember exactly. Thanks for sharing – Roko C. Buljan Jun 07 '23 at 18:56
50

In modern browsers, you can simply use css:

th {
  position: sticky;
  top: 0;
  z-index: 2;
}
Gauss
  • 1,108
  • 14
  • 13
  • 11
    This incantation is incomplete... Care to post an example, or at least elaborate? – Liam Feb 10 '18 at 06:32
  • working good, but only applies to th. If we set the position sticky on thead then it does not work. – Vishal Feb 21 '18 at 19:04
  • 9
    This answer works when you wrap your `` with `
    ` which has `overflow-y: auto;` and some `max-height`. For example: `
    ...
    `
    – Minwork Sep 10 '19 at 08:07
  • @Minwork the purpose here is to keep header fixed while scrolling the content, not the whole table. – detay May 07 '20 at 15:19
  • 2
    Simplest solution! Although, as @Minwork mentioned, you have to put your table inside a div tag, and give a max-height to it for seeing the effect. – Fabrizio Valencia Apr 17 '22 at 06:09
  • Works brilliantly (checked on Chrome as well as Firefox). And as @Minwork pointed out, must wrap the table in div. **Let somebody mark this as the correct solution**, to help bring this out of obscurity. – carla Nov 10 '22 at 10:04