HTML Scrolling Table With Fixed Headers jQuery Plugin
I have been searching for a nice simple jQuery plugin to convert a HTML table element to a scrolling element with a fixed header. The following demo page includes three tables. The top one has the desired effect including CSS styles. The middle one has the CSS styles, but has not had the jQuery goodness applied. And the bottom table is the HTML before applying CSS and the jQuery.
The screen shot above is an example of the scrollable table with data in it.
There are a lot of jQuery grids / tables on the internet. Here is a Stack Overflow question with recommendations for the best options. I tried a number of these plugins and the configuration seemed a bit complex. I venture to guess the complexity comes with trying to support many options (paging, JSON, XML…etc). This plugin is not being created to compete at that level, but rather provide a simpler alternative. I wanted to be able to create a scrolling table by applying the following jQuery
$("table#person").createScrollableTable({ width: '800px', height: '400px' });
to a semantic HTML table like the following
<table id="person" class="tbl1"> <thead> <tr> <th>Id</th> <th>First Name</th> <th>Last Name</th> <th>Birthdate</th> <th>Motto</th> </tr> </thead> <tbody> <tr> <td class="id">0</td> <td class="fname">Tim</td> <td class="lname">Brown</td> <td class="bdate">1/18/1947</td> <td class="motto">Id ligula sodales et tincidunt sagittis lectus maecenas tincidunt lorem.</td> </tr> <tr> <td class="id">1</td> <td class="fname">Dana</td> <td class="lname">Washington</td> <td class="bdate">9/2/1994</td> <td class="motto">Porta interdum in posuere donec duis ullamcorper nibh amet nunc ligula rutrum ante ac.</td> </tr> <!-- Additional rows removed for brevity. --> </tbody> </table>
The HTML can be generated using what ever type of server language that you favor. The important things to note:
- The JavaScript is simple. Use a jQuery selector and call the function with desired width & height.
- The HTML is semantic. The HTML is very bare-bones. The ‘tbl1’ class only adds style to the table elements. The class elements on the HTML td elements is not required, but is used to set column widths via CSS for the table.
Concept:
I first tried to create the minimal and cleanest HTML & CSS necessary to create the effect. I ended up with the following HTML and demo:
<div id="person_table_wrap" style="border: solid 1px #888; overflow: hidden; display: inline-block;"> <div id="person_head_wrap" style="overflow: hidden; width: 800px;"> <table id="person" class="tbl1"> <thead> <tr> <th>Id</th> <th>First Name</th> <th>Last Name</th> <th>Birthdate</th> <th>Motto</th> </tr> </thead> </table> </div> <div id="person_body_wrap" style="overflow: auto; width: 800px; height: 400px;"> <table id="person" class="tbl1"> <tbody> <tr> <td>0</td> <td>Tim</td> <td>Brown</td> <td>1/18/1947</td> <td>Id ligula sodales et tincidunt sagittis lectus maecenas tincidunt lorem.</td> </tr> <tr> <td>1</td> <td>Dana</td> <td>Washington</td> <td>9/2/1994</td> <td>Porta interdum in posuere donec duis ullamcorper nibh amet nunc ligula rutrum ante ac.</td> </tr> <!-- Additional rows removed for brevity. --> </table> </div> </div>
So what have we done? The biggest transformation has been to split the table header and body into separate HTML table elements. Each of these tables includes a wrapper HTML ‘div’ element (a head wrap and a body wrap). The result is placed inside another HTML ‘div’ container (a table wrap). It is simple enough to do this transformation with jQuery, but we still have a problem.
Notice from the demo or the above screen shot, the columns do not align. This problem arises because each table is allowed to set column widths independently. Using CSS to force all but the last (need to allow it to be the ‘expando’) columns to be the same width will fix this problem and you will get the following:
This is very close to what we want, except for the case where we want the columns to auto-size. Using a bit more jQuery, we can automatically set the column widths in the table header to the same as the widths in the table body.
That is the recipe for this jQuery plugin. Now, let’s get to the plugin code.
Plugin Code:
Here is the complete jQuery plugin:
(function($) { $.fn.createScrollableTable = function(options) { var defaults = { width: '400px', height: '300px', border: 'solid 1px #888' }; var options = $.extend(defaults, options); return this.each(function() { var table = $(this); prepareTable(table); }); function prepareTable(table) { var tableId = table.attr('id'); // wrap the current table (will end up being just body table) var bodyWrap = table.wrap('<div></div>') .parent() .attr('id', tableId + '_body_wrap') .css({ width: options.width, height: options.height, overflow: 'auto' }); // wrap the body var tableWrap = bodyWrap.wrap('<div></div>') .parent() .attr('id', tableId + '_table_wrap') .css({ overflow: 'hidden', display: 'inline-block', border: options.border }); // clone the header var headWrap = $(document.createElement('div')) .attr('Id', tableId + '_head_wrap') .prependTo(tableWrap) .css({ width: options.width, overflow: 'hidden' }); var headTable = table.clone(true) .attr('Id', tableId + '_head') .appendTo(headWrap) .css({ 'table-layout': 'fixed' }); var bufferCol = $(document.createElement('th')) .css({ width: '100%' }) .appendTo(headTable.find('thead tr')); // remove the extra html headTable.find('tbody').remove(); table.find('thead').remove(); // size the header columns to match the body var allBodyCols = table.find('tbody tr:first td'); headTable.find('thead tr th').each(function(index) { var desiredWidth = getWidth($(allBodyCols[index])); $(this).css({ width: desiredWidth + 'px' }); }); } function getWidth(td) { if ($.browser.msie) { return $(td).outerWidth() - 1; } if ($.browser.mozilla) { return $(td).width(); } if ($.browser.safari) { return $(td).outerWidth(); } return $(td).outerWidth(); }; }; })(jQuery);
There are only three configurable options that set the width, height and border. The width is applied to the body and head wrapper HTML ‘div’ elements. The height is only applied to the body wrap. The border is applied to the table wrap.
The ‘prepareTable’ function is where the semantic HTML table is transformed into two tables. First a HTML ‘div’ is wrapped around the table to form the body wrap. This table currently contains ‘thead’ and ‘tbody’ elements but not for long. Then a HTML ‘div’ is wrapped around this whole thing to form the table wrap. Then a new HTML ‘div’ element is prepended to the table wrap to become the head wrap. A clone of the table is then appended to the head wrap.
To enable the automatic column width setting an empty trailing column, ‘bufferCol’, is added to the table header. This is the “secret sauce” in the plugin. This element acts like an ‘expando’ and allows the other elements to have a fixed width. Finally, the HTML ‘tbody’ element in the head wrapper and the HTML ‘thead’ in the body wrapper are removed.
This completes the HTML transformation. Along the way the required CSS is added to the elements. The final bit of jQuery goodness sets the width of the columns in the header to the width of the columns in the footer. There is a bit of browser variation that is taken care of in the ‘getWidth’ function.
Table CSS:
Although not necessary for the jQuery plugin, I thought that I would provide the CSS that I am using to style the tables.
/* CSS Reset ----------------------------------------------------------*/ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td { margin: 0; padding: 0; border: 0; outline: 0; font-size: 100%; vertical-align: baseline; background: transparent; } table { border-collapse: collapse; border-spacing: 0; } /* h1 style ----------------------------------------------------------*/ h1 { font-size: 20px; text-align: center; background-color: #ffffdd; border: solid 1px #888; margin: 0 0 10px 0; padding: 5px; } /* Table style ----------------------------------------------------------*/ table.tbl1 { width: 100%; } table.tbl1 thead { background: #A3C2EA url('../images/tbl_head_bg.png') repeat-x scroll 0 0; color: #333; width: 100%; } table.tbl1 tbody { width: 100%; } table.tbl1 tr td, table.tbl1 tr th { border: solid 1px #888; margin: 0; padding: 4px 0; } table.tbl1 tr td { text-align: center; background-color: #fff; color: #444; } table.tbl1 tbody tr.alt td { background-color: #e4ecf7; } table.tbl1 tr.over td, table.tbl1 tbody tr.alt.over td { background-color: #bcd4ec; color: #000; } table.tbl1 a { color: #2a4a73; text-decoration: underline; } table.tbl1 a:hover { text-decoration: none; } table.tbl1 a:visited { color: #bccedc; } /* Define column widths ----------------------------------------------------------*/ table.tbl1 td.id { width: 100px; } table.tbl1 td.fname { width: 100px; } table.tbl1 td.lname { width: 100px; } table.tbl1 td.bdate { width: 100px; }
Summary:
The plugin works using Internet Explorer 8, FireFox 3, Chrome 4, and Safari 4. Please let me know if you find issues or have suggestions for improvements.
This is great code. However, one issue I ran into is if a column header cell gets auto-formatted to be wider than the corresponding data cell, the header cell gets chopped off. This is becaue the code sets the header cell width equal to the data cell width without checking to see if the header cell is wider.
I haven’t done it yet, but I”m sure it’s easy enough to change it so that it sets the width of both the header and data cell to whichever one is greater to begin with.
Hi Rob,
Thanks for reading and taking the time to respond. I will look into making the code a bit more robust to handle this situation.
Bob
hi craven,
your jquery plugin is great. i used it on my project.
i have a problem when displaying other table with different column numbers using same plugin. in one table, the header and body aligned correctly after editing subtraction value in getWidth function. but when opening other table, the header and body is not aligned because have different number of columns.
the solution is, i make some modifications on your code in scrolltable.js as follows :
1. in line 57 i changed the width value for bufferCol from 100% to 17px. it’s because the table showing a half square in firefox browser.
2. below line 62 (headTable.find(‘tbody’).remove();), i added code to remove footer in header div :
headTable.find('tfoot').remove();
3. below line 66 (assigning value of allBodyCols), i added codes to set background color for bufferCol and handling aligning width of header and body. the idea is adding column numbers as parameter in function getWidth(). here is the codes :
var allHeadCols = headTable.find('thead tr th');
var total_cols = headTable.find('thead tr th').length;
var cols = total_cols - headTable.find('.hidden').length;
var bgcolor = $(allHeadCols[total_cols-2]).css('background-color');
$(allHeadCols[total_cols-1]).css('background-color', bgcolor);
headTable.find('thead tr th').each(function(index) {
var desiredWidth = getWidth($(allBodyCols[index]),cols);
$(this).css({ width: desiredWidth + 'px' });
});
}
function getWidth(td,cols) {
if ($.browser.msie) { return $(td).outerWidth() - (22-cols); }
if ($.browser.mozilla) { return $(td).outerWidth() - (22-cols); }
if ($.browser.safari) { return $(td).outerWidth(); }
return $(td).outerWidth();
};
Thanks for the feedback. I will look at your changes and try to get them in the next version.
Hello,
great plugin, but I run into an issue when using width and height in percentage instead of pixels.
As I have a dynamic layout I need these values given in percentage. So, is there any solution/workaround for that?
Daniel
Hi Daniel,
I have not (obviously) used percentages instead of pixels. I took a quick look at the code. It appears that I have a number of ‘px’ hard coded. I suspect that is causing the issue. I don’t have the time to play with the code right now. I will try to swing back around to it later.
Bob
Hi Bob, have you had a chance to look into this?
Cheers,
Anton
Hi Anton,
I haven’t been able to allocate any time to look into this. Sorry.
Bob
Have not played with plugin. But, looked at the demo. Is it possible to make the header scollable horizontally for large tables? Thanks,
Joan
Hi Joan,
Thanks for stopping by. I suspect it is possible. I would probably try dynamically wrapping the table and header into a ‘div’ that has a fixed with with overflow set to scroll. I suspect that playing around with the CSS a bit and you can achieve what you are asking. It is not something that the current plugin supports.
Bob
How difficult do you think it would be to implement a fixed footer as well? So the end result was a scrollable table with a fixed header AND footer.
Probably not that difficult. I am trying to imagine what would go in the footer. If it were just a copy of the header, then the same type of scheme could be used again. Just add another table with the fixed footer. This could probably be done regardless. If the content isn’t the same as the header, why not just add it as part of your dom?
Hey Bob, I really like the scrollable table you designed. I’m trying to use it myself, and it is working very nicely except that I’m receiving an js invalid error msg on IE8. The output looks just like I want it to, so I’m not sure what’s causing the error. Any idea what would cause the following to be generated:
Webpage error details
User Agent: Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64)
Timestamp: Tue, 13 Mar 2012 20:51:06 UTC
Message: Invalid argument.
Line: 12
Char: 12949
Code: 0
URI: http://127.0.0.1/wo/scripts/jquery-1.3.2.min.js
Hi Matt,
Not sure of what is causing the error. Is there a URL you can send me (bob.cravens at gmail.com) and I can take a look. If I find an issue, then I can fold the fix back into the mix for everyone.
Thanks,
Bob
I have the same problem of Matt.
User Agent: Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; .NET CLR 1.1.4322; .NET CLR 2.0.50727; InfoPath.2; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)
Timestamp: Mon, 28 May 2012 07:15:25 UTC
Message: Invalid argument.
Line: 12
Char: 12949
Code: 0
URI: http://ittools/s1/scripts/jquery-1.3.2.min.js
is there any option to get a n aligned output without speciyfying the width
I suppose you could measure the column widths with jQuery of the data table and then set the header widths. I haven’t tried though.
In chrome don’t align perfect, to solve the problem adding this:
if ($.browser.webkit) { return $(td).outerWidth() – 1; }
below
function getWidth(td) {
Hi
I need a help, when i use jquery plugins it works well. but some columns are hidden by default and when the user needs to see those hidden columns, there is a seperate link to show those columns. So the problem is when the user clicks the links to show those hidden columns the header for those are not seen since the header is cloned. so can you help me out.
For show/hide columns i have implemented in javascript
only the fixed header part have done in jquery
can you help me out please.
I’ve learn several excellent stuff here. Definitely worth bookmarking for revisiting. I surprise how much effort you set to create the sort of excellent informative website.
Many many thanks for a great component!!! nice work 🙂
Hi,
This is really useful, I need to tweak it to make it work on our project so I’ve put it on github, hope you don’t mind.
https://github.com/stuaxo/scrolltable
If you want to put the original somewhere then I can make push requests.
When it comes to licensing would MIT do (fairly permissive) ?
Cheers,
Stuart
hi this is the source code support colspan header??
Hi, Thank you so so much for this share!!! very nice work..
Thank you so much for this plugin !! very great work !
function FixFF() {
//table body
var TableWrap = $(document.createElement(‘div’))
.attr(‘Id’, ‘Panel1_table_wrap’)
.prependTo($(“#Panel1”))
.css({
width: ‘455px’,
‘overflow-x’: ‘hidden’,
height: ‘100px’
});
//table head
var headWrap = $(document.createElement(‘div’))
.attr(‘Id’, ‘Panel1_head_wrap’)
.prependTo($(“#Panel1”))
.css({
width: ‘455px’,
overflow: ‘hidden’
});
var headTable = $(“#Cust”).clone(true)
.attr(‘Id’, ‘Panel1_head’)
.appendTo(headWrap)
.css({
‘table-layout’: ‘fixed’
});
var bufferCol = $(document.createElement(‘th’))
.css({
width: ‘100%’
})
.appendTo(headTable.find(‘thead tr’));
var bodyTable = $(“#Cust”).clone(true)
.attr(‘Id’, ‘Panel1_body’)
.appendTo(TableWrap)
.css({
‘table-layout’: ‘fixed’
});
headTable.find(‘tbody’).remove();
bodyTable.find(‘thead’).remove();
var allBodyCols = $(“#Cust”).find(‘tbody tr:first td’);
bodyTable.find(‘tbody tr td’).each(function (index) {
var desiredW = getWidth($(allBodyCols[index]));
$(this).css({ width: desiredW + ‘px’ });
});
headTable.find(‘thead tr th’).each(function (index) {
var desiredWidth = getWidth($(allBodyCols[index]));
$(this).css({ width: desiredWidth + ‘px’ });
});
$(“#Cust”).hide();
}
function getWidth(td) {
return $(td).attr(‘clientWidth’);
};
Hello,
I’m trying to use it nowadays, but it’s incompatible with the new version of JQuery :S
Do you know how to solve this problem?
I am very thankful.
That is a real helpful code,can you please sort the table data when click on the headers …..Thanks in advance ..
Just a quick thank you for the time you put in. I’ve been meaning to write one myself for sometime. Thanks again.
this example works great, i have been looking for the fixed header/scrollable body solutions and ended up here, great example.
My header row is all jumbled up for some reason…