HTML select tag with fixed headline? - html

I'm looking for a solution to make a scrollable select box with heading like in the picture. If I use a table, I can't choose any of the lines. I want the user to mark one option, so he can delete it when he hits the button. I'm looking for a simple html+css solution if it's possible.
If I use the select-tag I'm not sure how to add any headings.
Every suggestion is welcome!
What I've tried so far:
div.tableContainer {
clear: both;
border: 1px solid #963;
height: 285px;
overflow: auto;
width: 900px
}
div.tableContainer table {
float: left;
}
thead.fixedHeader tr {
position: relative;
}
thead.fixedHeader th {
background: #C96;
border-left: 1px solid #EB8;
border-right: 1px solid #B74;
border-top: 1px solid #EB8;
font-weight: normal;
padding: 4px 3px;
text-align: left
}
html>body tbody.scrollContent {
display: block;
height: 262px;
overflow: auto;
width: 100%
}
html>body thead.fixedHeader {
display: table;
overflow: auto;
width: 100%
}
tbody.scrollContent td,
tbody.scrollContent tr.normalRow td {
background: #FFF;
border-bottom: none;
border-left: none;
border-right: 1px solid #CCC;
border-top: 1px solid #DDD;
padding: 2px 3px 3px 4px
}
tbody.scrollContent tr.alternateRow td {
background: #EEE;
border-bottom: none;
border-left: none;
border-right: 1px solid #CCC;
border-top: 1px solid #DDD;
padding: 2px 3px 3px 4px
}
<div id="tableContainer" class="tableContainer">
<table border="0" cellpadding="0" cellspacing="0" width="100%" class="scrollTable">
<thead class="fixedHeader">
<tr>
<th width="20%">Header 1</th>
<th width="30%">Header 2</th>
<th width="50%">Header 3</th>
</tr>
</thead>
<tbody class="scrollContent">
<tr>
<td width="20%">Cell Content 1</td>
<td width="30%">Cell Content 2</td>
<td width="48%">Cell Content 3</td>
</tr>
<tr>
<td>More Cell Content 1</td>
<td>More Cell Content 2</td>
<td>More Cell Content 3</td>
</tr>
<tr>
<td>Even More Cell Content 1 Even More Cell Content 1 Even More Cell Content 1 Even More Cell Content 1</td>
<td>Even More Cell Cont asdkfh asoid hfoas ghdfoasgdojf gasdkjfgasd jfgaskjdfg kjasdgf kjasd gjkasgdfkj agskjdfgaskdjfgent 2</td>
<td>Even More Cell Content 3</td>
</tr>
<tr>
<td>More Cell Content 1</td>
<td>More Cell Content 2</td>
<td>More Cell Content 3</td>
</tr>
<tr>
<td>Even More Cell Content 1 Even More Cell Content 1 Even More Cell Content 1 Even More Cell Content 1</td>
<td>Even More Cell Cont asdkfh asoid hfoas ghdfoasgdojf gasdkjfgasd jfgaskjdfg kjasdgf kjasd gjkasgdfkj agskjdfgaskdjfgent 2</td>
<td>Even More Cell Content 3</td>
</tr>
<tr>
<td>More Cell Content 1</td>
<td>More Cell Content 2</td>
<td>More Cell Content 3</td>
</tr>
<tr>
<td>Even More Cell Content 1 Even More Cell Content 1 Even More Cell Content 1 Even More Cell Content 1</td>
<td>Even More Cell Cont asdkfh asoid hfoas ghdfoasgdojf gasdkjfgasd jfgaskjdfg kjasdgf kjasd gjkasgdfkj agskjdfgaskdjfgent 2</td>
<td>Even More Cell Content 3</td>
</tr>
<tr>
<td>More Cell Content 1</td>
<td>More Cell Content 2</td>
<td>More Cell Content 3</td>
</tr>
<tr>
<td>Even More Cell Content 1 Even More Cell Content 1 Even More Cell Content 1 Even More Cell Content 1</td>
<td>Even More Cell Cont asdkfh asoid hfoas ghdfoasgdojf gasdkjfgasd jfgaskjdfg kjasdgf kjasd gjkasgdfkj agskjdfgaskdjfgent 2</td>
<td>Even More Cell Content 3</td>
</tr>
<tr>
<td>More Cell Content 1</td>
<td>More Cell Content 2</td>
<td>More Cell Content 3</td>
</tr>
<tr>
<td>Even More Cell Content 1 Even More Cell Content 1 Even More Cell Content 1 Even More Cell Content 1</td>
<td>Even More Cell Cont asdkfh asoid hfoas ghdfoasgdojf gasdkjfgasd jfgaskjdfg kjasdgf kjasd gjkasgdfkj agskjdfgaskdjfgent 2</td>
<td>Even More Cell Content 3</td>
</tr>
<tr>
<td>More Cell Content 1</td>
<td>More Cell Content 2</td>
<td>More Cell Content 3</td>
</tr>
<tr>
<td>Even More Cell Content 1 Even More Cell Content 1 Even More Cell Content 1 Even More Cell Content 1</td>
<td>Even More Cell Cont asdkfh asoid hfoas ghdfoasgdojf gasdkjfgasd jfgaskjdfg kjasdgf kjasd gjkasgdfkj agskjdfgaskdjfgent 2</td>
<td>Even More Cell Content 3</td>
</tr>
<tr>
<td>More Cell Content 1</td>
<td>More Cell Content 2</td>
<td>More Cell Content 3</td>
</tr>
<tr>
<td>Even More Cell Content 1 Even More Cell Content 1 Even More Cell Content 1 Even More Cell Content 1</td>
<td>Even More Cell Cont asdkfh asoid hfoas ghdfoasgdojf gasdkjfgasd jfgaskjdfg kjasdgf kjasd gjkasgdfkj agskjdfgaskdjfgent 2</td>
<td>Even More Cell Content 3</td>
</tr>
<tr>
<td>More Cell Content 1</td>
<td>More Cell Content 2</td>
<td>More Cell Content 3</td>
</tr>
<tr>
<td>Even More Cell Content 1 Even More Cell Content 1 Even More Cell Content 1 Even More Cell Content 1</td>
<td>Even More Cell Cont asdkfh asoid hfoas ghdfoasgdojf gasdkjfgasd jfgaskjdfg kjasdgf kjasd gjkasgdfkj agskjdfgaskdjfgent 2</td>
<td>Even More Cell Content 3</td>
</tr>
</tbody>
</table>
</div>

I recommend you learn to use DataTables which is well maintained & documented library which has a number of good plugins and a community of developer users.
var myDTable;
$(document).ready(function() {
//An example of JavaScript sourced data, though you can instead have your thead and tbody rows defined in your HTML directly
var dataSet = [];
for(let i = 0; i < 100; i++) {
dataSet.push({id:i, name:"Bob Dylan", email:"bob#columbia.com", phone:"512-973-2991"});
}
myDTable = $('#myTable').DataTable( {
data: dataSet,
columns: [
{ title: "Name", data:"name" },
{ title: "Email", data:"email"},
{ title: "Phone", data:"phone" }
],
//select:true Allows multiple row selection
select:"single",
pageLength:100,
fixedHeader: true,
dom:"frtip"
});
//Listening for event click
$(document).on("click", "#myTable tbody tr", function() {
//How to get the clicked row data
var rowData = myDTable.row(this).data();
console.log(rowData);
//Get the selected row data
var selectedRows = myDTable.rows({selected:true});
var numSelectedRows = selectedRows.count();
//Show hide delete button accordingly
if(numSelectedRows == 0) {
$('#deleteRowBtn').hide();
} else {
$('#deleteRowBtn').show();
}
});
$(document).on("click", "#deleteRowBtn", function() {
var selectedRows = myDTable.rows({selected:true});
var rowToDelete = selectedRows.data()[0];
console.log("Delete this row: ");
console.log(rowToDelete);
});
});
table.dataTable tbody>tr.selected, table.dataTable tbody>tr>.selected {
background-color: #417fd8 !important;
color: white !important;
}
table.dataTable.display tbody>tr.selected>.sorting_1, table.dataTable.order-column.stripe tbody>tr.selected>.sorting_1 {
background-color: #2868c3 !important;
}
table.dataTable tbody>tr {
cursor:pointer
}
table.dataTable thead th, table.dataTable thead td {
padding: 2px 18px;
border: 1px solid #efefef;
border-collapse: collapse;
}
#deleteRowBtn {
position: fixed;
width: 200px;
margin-left: -100px;
left: 50%;
z-index: 1000;
text-align: center;
padding: 5px 10px;
box-sizing: border-box;
bottom: 10px;
background-color: darkred;
color: white;
cursor: pointer;
border-radius: 10px;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<link rel="stylesheet" href="https://cdn.datatables.net/1.10.19/css/jquery.dataTables.min.css" type="text/css">
<script src="https://cdn.datatables.net/1.10.19/js/jquery.dataTables.min.js"></script>
<link rel="stylesheet" href="https://cdn.datatables.net/fixedheader/3.1.5/css/fixedHeader.dataTables.min.css" type="text/css">
<script src="https://cdn.datatables.net/fixedheader/3.1.5/js/dataTables.fixedHeader.min.js"></script>
<link rel="stylesheet" href="https://cdn.datatables.net/select/1.2.7/css/select.dataTables.min.css" type="text/css">
<script src="https://cdn.datatables.net/select/1.2.7/js/dataTables.select.min.js"></script>
<table id="myTable" class="display"></table>
<div id="deleteRowBtn" style="display:none">Delete Selected Row</div>
More resources:
https://datatables.net/
https://datatables.net/reference/option/select
https://datatables.net/reference/option/fixedHeader

Related

How to make table header stretch to full width?

I have a table where there are more columns in the table rows than there are in the header:
How is it possible to make the header stretch to full width, with html/css, but without adding an empty header column ?
My code:
HTML:
<div id="thirdTable">
<table>
<tbody>
<tr>
<th>First</th>
<th>Second</th>
</tr>
<tr>
<td>data goes here</td>
<td>data goes here</td>
<td>data goes here</td>
</tr>
<tr>
<td>more data here</td>
<td>more data here</td>
<td>more data here</td>
</tr>
</tbody>
</table>
</div>;
CSS:
#thirdTable table {
border: none;
border-collapse: collapse;
}
#thirdTable tr {
background: linear-gradient(to right, #0033FF, #3399FF);
}
#thirdTable td {
color: white;
border: none;
padding: 15px 8px 15px 8px;
}
#thirdTable th {
color: #FFCC00;
border: none;
}
Using colspan
I do not understand why you would want your table to not have all the headers for all the table cells but I assume you would want something like this, where the a th can occupy 2 columns or even more if you need.
#thirdTable table {
border: none;
border-collapse: collapse;
}
#thirdTable tr {
background: linear-gradient(to right, #0033FF, #3399FF);
}
#thirdTable td {
color: white;
border: none;
padding: 15px 8px 15px 8px;
}
#thirdTable th {
color: #FFCC00;
border: none;
}
<div id="thirdTable">
<table>
<tbody>
<tr>
<th>First</th>
<th colspan="2">Second</th>
</tr>
<tr>
<td>data goes here</td>
<td>data goes here</td>
<td>data goes here</td>
</tr>
<tr>
<td>more data here</td>
<td>more data here</td>
<td>more data here</td>
</tr>
</tbody>
</table>
</div>
<hr />
<div id="thirdTable">
<table>
<tbody>
<tr>
<th colspan="2">First</th>
<th>Second</th>
</tr>
<tr>
<td>data goes here</td>
<td>data goes here</td>
<td>data goes here</td>
</tr>
<tr>
<td>more data here</td>
<td>more data here</td>
<td>more data here</td>
</tr>
</tbody>
</table>
</div>
You can use the colspan attribute:
<th colspan="2">Second</th>

align table header in one table to body in another table

I have two html tables. The first table is for the header and the second table is for the body. The reason for having two separate table is to be able to have a fixed header and a scrollable body.
How will I make the table header aligned with the table body?
Below is my code. current output
HTML
.scroll {
height: 100px;
overflow: auto;
}
#containerdiv {
width: 80%;
border: 2px solid brown;
}
<div id="containerdiv">
<div class="header">
<table>
<thead>
<!-- column names are going to be generated automatially with angular -->
<tr>
<th style="border: red solid 2px">Id</th>
<th style="border: red solid 2px">Customer Name</th>
<th style="border: red solid 2px">Address</th>
<th style="border: red solid 2px">Phone</th>
</tr>
</thead>
<!-- this is to set size for the column headers-->
<tbody>
<tr>
<td style="width:2%;"></td>
<td style="width:5%;"></td>
<td style="width:6%;"></td>
<td style="width:4%;"></td>
</tr>
</tbody>
</table>
</div>
<div class="scroll">
<table id="tablebody">
<tr>
<td style="width:4%;border: red solid 2px;">1</td>
<td style="width:5%;border: red solid 2px;">Customer 1</td>
<td style="width:6%;border: red solid 2px;">Address 1</td>
<td style="width:4%;border: red solid 2px;">phone number 1</td>
</tr>
<tr>
<td style="width:4%;border: red solid 2px;">2</td>
<td style="width:5%;border: red solid 2px;">Customer 2</td>
<td style="width:6%;border: red solid 2px;">Address 2</td>
<td style="width:4%;border: red solid 2px;">phone number 2</td>
</tr>
<tr>
<td style="width:4%;border: red solid 2px;">3</td>
<td style="width:5%;border: red solid 2px;">Customer 3</td>
<td style="width:6%;border: red solid 2px;">Address 3</td>
<td style="width:4%;border: red solid 2px;">phone number 3</td>
</tr>
<tr>
<td style="width:4%;border: red solid 2px;">4</td>
<td style="width:5%;border: red solid 2px;">Customer 4</td>
<td style="width:6%;border: red solid 2px;">Address 4</td>
<td style="width:4%;border: red solid 2px;">phone number 4</td>
</tr>
<tr>
<td style="width:4%;border: red solid 2px;">5</td>
<td style="width:5%;border: red solid 2px;">Customer 5</td>
<td style="width:6%;border: red solid 2px;">Address 5</td>
<td style="width:4%;border: red solid 2px;">phone number 5</td>
</tr>
</table>
</div>
</div>
Your main problem is that your widths 1. don't match, and more importantly 2. don't make sense.
Note that in your header, the first column inside the tbody is set at width: 2%, but the equivalent column in the next table is at width: 4%. Those values are gonna have to match, or things won't line up.
But more importantly, assuming we change that 2% to 4%, your columns only add up to 19% wide.
Percentage widths are based on their parents, not on the overall page width. So since you have set #containerdiv to 80% wide, by setting a column to 4% wide, you're making that column 4% of 80% of the page width = 0.32% of the page width.
What you really want to do is think of the table as 100% wide, and divide your columns up so their widths add up to 100%.
#containerdiv {
width: 80%;
}
#containerdiv table {
width: 100%;
}
#containerdiv table td:nth-child(1) { /* first column */
width: 15%;
}
/* and so on for other columns */
The other thing you can do to make this easier is to use box-sizing to include borders in the widths of your columns, so if you want a column to be, say, 25% wide with a 4 pixel border, you don't have to worry about setting a width like calc(25% - 4px). That is, if you want to continue with non-collapsed borders and spacing between table cells.
Lastly, note that your border shorthand property was out of order. It should be border-width, border-style, then border-color.
In this example, I took all your inline styles out and moved them into the stylesheet to make things easier to visualize, but if you're going to end up with inline styles in your app, that's fine. Follow the principles here, and it'll work:
.scroll {
height: 100px;
overflow: auto;
}
#containerdiv {
width: 80%;
border: 2px solid brown;
}
#containerdiv table {
width: 100%;
}
.header table {
padding-right: 17px;
}
#containerdiv table * {
box-sizing: border-box;
}
#containerdiv th,
#containerdiv td {
border: 2px solid red;
}
#containerdiv .header tbody td {
border: none;
}
#containerdiv th:nth-child(1),
#containerdiv td:nth-child(1) {
width: 15%;
}
#containerdiv th:nth-child(2),
#containerdiv td:nth-child(2) {
width: 30%;
}
#containerdiv th:nth-child(3),
#containerdiv td:nth-child(3) {
width: 30%;
}
#containerdiv th:nth-child(4),
#containerdiv td:nth-child(4) {
width: 25%;
}
<div id="containerdiv">
<div class="header">
<table>
<thead>
<!-- column names are going to be generated automatially with angular -->
<tr>
<th>Id</th>
<th>Customer Name</th>
<th>Address</th>
<th>Phone</th>
</tr>
</thead>
<!-- this is to set size for the column headers-->
<tbody>
<tr>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
</tbody>
</table>
</div>
<div class="scroll">
<table id="tablebody">
<tr>
<td>1</td>
<td>Customer 1</td>
<td>Address 1</td>
<td>phone number 1</td>
</tr>
<tr>
<td>2</td>
<td>Customer 2</td>
<td>Address 2</td>
<td>phone number 2</td>
</tr>
<tr>
<td>3</td>
<td>Customer 3</td>
<td>Address 3</td>
<td>phone number 3</td>
</tr>
<tr>
<td>4</td>
<td>Customer 4</td>
<td>Address 4</td>
<td>phone number 4</td>
</tr>
<tr>
<td>5</td>
<td>Customer 5</td>
<td>Address 5</td>
<td>phone number 5</td>
</tr>
</table>
</div>
</div>
Edit regarding scrollbar: I have fixed the scrollbar issue on PC by adding padding-right to the only first table, so that there is a gap to the right of it where the scrollbar appears next to the second table. I chose 17px as the value because that's the scrollbar width for the most common browsers on Windows. But note that this isn't a perfect solution because not all browsers' scrollbars are exactly the same width.
A real-life perfect execution of that approach would use JavaScript to detect the scrollbar width and then set the padding-right accordingly.
However, an alternate approach that doesn't require any JavaScript is to set the table widths in viewport units, for example, to 75vw.
That's because 100vw is equivalent to the entire width of the window, including the scrollbar, whereas 100% (on body) is equivalent to the window width to the left of the scrollbar. So basically using vw units would let you ignore the scrollbar so that both tables come out the same width. Just choose a value that's small enough to leave space for the scrollbar on the bottom table, and the top table will match, even though it doesn't have a scrollbar.
Finally, note that all of this is really just to get around problems you're causing for yourself by the overall approach you're using here of using two separate tables to achieve the fixed header. Really, in my opinion, you should only have one table and just freeze the thead in place and be done with it. Then you wouldn't even have the scrollbar issue. Revising the whole thing to work that way is beyond the scope of this question, but it's something for you to consider.
I would use one table and fix the header like below.
Note: in <th> tags, the text has to be placed twice: once in a <div> (this will be the fixed one) and once normally (this will stay inside the table structure and ensure column width). Latter one can be hidden with some additional styling.
thead th div {
position: fixed;
}
<table>
<thead>
<tr>
<th>
<div>header 1 which is longer</div>
header 1 which is longer
<!-- yes, it's needed! -->
</th>
<th>
<div>header 2</div>
header 2
<!-- yes, it's needed! -->
</th>
</tr>
</thead>
<tbody>
<tr>
<td>content 1</td>
<td>content 2</td>
</tr>
<tr>
<td>content 1</td>
<td>content 2</td>
</tr>
<tr>
<td>content 1</td>
<td>content 2</td>
</tr>
<tr>
<td>content 1</td>
<td>content 2</td>
</tr>
<tr>
<td>content 1</td>
<td>content 2</td>
</tr>
<tr>
<td>content 1</td>
<td>content 2</td>
</tr>
<tr>
<td>content 1</td>
<td>content 2</td>
</tr>
<tr>
<td>content 1</td>
<td>content 2</td>
</tr>
<tr>
<td>content 1</td>
<td>content 2</td>
</tr>
<tr>
<td>content 1</td>
<td>content 2</td>
</tr>
<tr>
<td>content 1</td>
<td>content 2</td>
</tr>
<tr>
<td>content 1</td>
<td>content 2</td>
</tr>
<tr>
<td>content 1</td>
<td>content 2</td>
</tr>
</tbody>
</table>

CSS: Fixed-first-column Table? (CSS only!)

I've been trying to figure out a way to take a standard table and create a fixed-column table where the first column is fixed while the rest scrolls. There's a couple ways that I think make sense, so I'll start with that.
The first way that makes sense to me is to simply break the table code format by creating a separate table as the column that we want to be by itself, something like this:
<div class="table-container">
<div class="table-column">
<table>
<thead><tr><th> </th></tr></thead>
<tbody>
<tr><td>Side Header 1</td></tr>
<tr><td>Side Header 2</td></tr>
<tr><td>Side Header 3</td></tr>
<tr><td>Side Header 4</td></tr>
<tr><td>Side Header 5</td></tr>
<tr><td>Side Header 6</td></tr>
</tbody>
</table>
</div>
<div class="table-column" style="overflow-x: auto;">
<table>
<thead>
<tr><th>Top Header 1</th><th>Top Header 2</th></tr>
</thead>
<tbody>
<tr><td>Row 1, Cell 1</td><td>Row 1, Cell 2</td></tr>
<tr><td>Row 2, Cell 1</td><td>Row 2, Cell 2</td></tr>
<tr><td>Row 3, Cell 1</td><td>Row 3, Cell 2</td></tr>
<tr><td>Row 4, Cell 1</td><td>Row 4, Cell 2</td></tr>
<tr><td>Row 5, Cell 1</td><td>Row 5, Cell 2</td></tr>
<tr><td>Row 6, Cell 1</td><td>Row 6, Cell 2</td></tr>
</tbody>
</table>
</div>
</div>
We make the first column have a <th> of just blank space so that the styling for the whole table still fits.
What I really want to do though is make this more of a dynamic process... Obviously in that case (especially using the word 'dynamic') I could just use some JS, but there must be a way to do this in CSS... but there doesn't seem to be anything solid online... so I thought I'd give it a go.
The closest I've been able to come is through using data-attribute:; and td::before, like this:
<div class="box-table">
<table class="text-center hover stripes">
<thead>
<tr>
<th data-label="Cat 1">Cat 1</th>
<th>Cat 2</th>
<th>Cat 3</th>
<th>Cat 4</th>
<th>Cat 5</th>
<th>Cat 6</th>
</tr>
</thead>
<tbody>
<tr>
<td data-label="Col 1">Col 1</td>
<td>Col 2</td>
<td>Col 3</td>
<td>Col 4</td>
<td>Col 5</td>
<td>Col 6</td>
</tr>
<tr>
<td data-label="Col 1">Col 1</td>
<td>Col 2</td>
<td>Col 3</td>
<td>Col 4</td>
<td>Col 5</td>
<td>Col 6</td>
</tr>
</tbody>
</table>
</div>
<style>
tr > th:first-child,
tr > td:first-child {
padding: 0;
}
tr > th:first-child::before,
tr > td:first-child::before {
content: attr(data-label);
display: inline-block;
position: fixed;
background: #fff;
border-right: 1px solid rgba(0,0,0,0.2);
letter-spacing: 1px;
padding: 0 0.75rem;
}
<style>
Here's a fiddle with what I've gotten so far: https://jsfiddle.net/wn5nonu3/
There's 2 issues I've run into:
The first is that because I've set the item to fixed, if the overflow of the table allows vertical scrolling then the fixed will obviously stay where they are fixed and appear out of line with the row.
The second issue is that I can't seem to style td::before (it seems to be showing 'inline' behavior regardless of what I change the display:; value to?).
Potential solution to the second problem is to remove the padding causing the row's to be larger, set the first column to a fixed width and add that width to td::before. I still can't fix the first problem though.
I thought I'd share in case anyone has any ideas about how this could possibly work, or whether or not the route I'm taking is even really doable?
jsFiddle: https://jsfiddle.net/wn5nonu3/ (same as one posted above, just for easy finding)
For the record: I know there's a number of great JS options, I just like to limit the amount of scripts I throw on my pages, and this just seems like something that would be useful.
One way you could make it is by using two tables with the same style, but it's a little hard to maintain. You have to make sure both tables are on the same line and that there's no space between them. When you just wrap the table with the actual content in a div that scrolls. Honestly, I wouldn't go there unless you really don't want to use JS, but it's your call.
.table-wrapper {
width: 100%;
white-space: nowrap;
}
.table-firstcolon {
display: inline-block;
vertical-align: top;
}
.table-firstcolon td,
.table-firstcolon th {
width: 60px;
}
.table-content-wrapper {
display: inline-block;
overflow-x: scroll;
vertical-align: top;
max-width: calc(100% - 60px);
}
.table-content-wrapper>table {
display: inline-block;
vertical-align: top;
}
/* Page Setup */
*,
*::after,
*::before {
box-sizing: inherit;
}
html {
box-sizing: border-box;
font-family: Arial, Helvetica, sans-serif;
font-size: 14px;
height: 100%;
line-height: 1.5;
width: 50%;
}
body {
min-height: 100%;
padding: 5px 10px;
/* this style is only for the fiddle, would be '0' */
width: 100%;
}
/* General Table Styling */
table {
background: #fdfdfd;
border-collapse: collapse;
border-spacing: 0;
}
th {
font-weight: bold;
}
thead,
tbody,
tfoot {
border: 1px solid #f1f1f1;
}
th, td {
padding: 6px;
text-align: left;
}
thead tr:first-child {
font-weight: bold;
border-bottom: 2px solid #eee;
}
tr:nth-child(even) {
background-color: rgba(0, 0, 0, 0.025);
}
tr:hover td {
background-color: rgba(0, 0, 0, 0.04);
color: #000;
}
<div class="table-wrapper">
<table class="table-firstcolon text-center hover stripes">
<thead>
<tr>
<th> </th>
</tr>
</thead>
<tbody>
<tr>
<th>Row 1</th>
</tr>
<tr>
<th>Row 2</th>
</tr>
<tr>
<th>Row 3</th>
</tr>
<tr>
<th>Row 4</th>
</tr>
<tr>
<th>Row 5</th>
</tr>
<tr>
<th>Row 6</th>
</tr>
</tbody>
</table><!--
--><div class="table-content-wrapper">
<table>
<thead>
<tr>
<th>Column A</th>
<th>Column B</th>
<th>Column C</th>
<th>Column D</th>
<th>Column E</th>
</tr>
</thead>
<tbody>
<tr>
<td>A1</td>
<td>B1</td>
<td>C1</td>
<td>D1</td>
<td>E1</td>
</tr>
<tr>
<td>A2</td>
<td>B2</td>
<td>C2</td>
<td>D2</td>
<td>E2</td>
</tr>
<tr>
<td>A3</td>
<td>B3</td>
<td>C3</td>
<td>D3</td>
<td>E3</td>
</tr>
<tr>
<td>A4</td>
<td>B4</td>
<td>C4</td>
<td>D4</td>
<td>E4</td>
</tr>
<tr>
<td>A5</td>
<td>B5</td>
<td>C5</td>
<td>D5</td>
<td>E5</td>
</tr>
<tr>
<td>A6</td>
<td>B6</td>
<td>C6</td>
<td>D6</td>
<td>E6</td>
</tr>
</tbody>
</table>
</div>
</div>

Scroll inside tbody [duplicate]

Is there a cross-browser CSS/JavaScript technique to display a long HTML table such that the column headers stay fixed on-screen and do not scroll with the table body. Think of the "freeze panes" effect in Microsoft Excel.
I want to be able to scroll through the contents of the table, but to always be able to see the column headers at the top.
This can be cleanly solved in four lines of code.
If you only care about modern browsers, a fixed header can be achieved much easier by using CSS transforms. Sounds odd, but works great:
HTML and CSS stay as-is.
No external JavaScript dependencies.
Four lines of code.
Works for all configurations (table-layout: fixed, etc.).
document.getElementById("wrap").addEventListener("scroll", function(){
var translate = "translate(0,"+this.scrollTop+"px)";
this.querySelector("thead").style.transform = translate;
});
Support for CSS transforms is widely available except for Internet Explorer 8-.
Here is the full example for reference:
document.getElementById("wrap").addEventListener("scroll",function(){
var translate = "translate(0,"+this.scrollTop+"px)";
this.querySelector("thead").style.transform = translate;
});
/* Your existing container */
#wrap {
overflow: auto;
height: 400px;
}
/* CSS for demo */
td {
background-color: green;
width: 200px;
height: 100px;
}
<div id="wrap">
<table>
<thead>
<tr>
<th>Foo</th>
<th>Bar</th>
</tr>
</thead>
<tbody>
<tr><td></td><td></td></tr>
<tr><td></td><td></td></tr>
<tr><td></td><td></td></tr>
<tr><td></td><td></td></tr>
<tr><td></td><td></td></tr>
<tr><td></td><td></td></tr>
<tr><td></td><td></td></tr>
<tr><td></td><td></td></tr>
<tr><td></td><td></td></tr>
<tr><td></td><td></td></tr>
<tr><td></td><td></td></tr>
<tr><td></td><td></td></tr>
</tbody>
</table>
</div>
I was looking for a solution for this for a while and found most of the answers are not working or not suitable for my situation, so I wrote a simple solution with jQuery.
This is the solution outline:
Clone the table that needs to have a fixed header, and place the
cloned copy on top of the original.
Remove the table body from top table.
Remove the table header from bottom table.
Adjust the column widths. (We keep track of the original column widths)
Below is the code in a runnable demo.
function scrolify(tblAsJQueryObject, height) {
var oTbl = tblAsJQueryObject;
// for very large tables you can remove the four lines below
// and wrap the table with <div> in the mark-up and assign
// height and overflow property
var oTblDiv = $("<div/>");
oTblDiv.css('height', height);
oTblDiv.css('overflow', 'scroll');
oTbl.wrap(oTblDiv);
// save original width
oTbl.attr("data-item-original-width", oTbl.width());
oTbl.find('thead tr td').each(function() {
$(this).attr("data-item-original-width", $(this).width());
});
oTbl.find('tbody tr:eq(0) td').each(function() {
$(this).attr("data-item-original-width", $(this).width());
});
// clone the original table
var newTbl = oTbl.clone();
// remove table header from original table
oTbl.find('thead tr').remove();
// remove table body from new table
newTbl.find('tbody tr').remove();
oTbl.parent().parent().prepend(newTbl);
newTbl.wrap("<div/>");
// replace ORIGINAL COLUMN width
newTbl.width(newTbl.attr('data-item-original-width'));
newTbl.find('thead tr td').each(function() {
$(this).width($(this).attr("data-item-original-width"));
});
oTbl.width(oTbl.attr('data-item-original-width'));
oTbl.find('tbody tr:eq(0) td').each(function() {
$(this).width($(this).attr("data-item-original-width"));
});
}
$(document).ready(function() {
scrolify($('#tblNeedsScrolling'), 160); // 160 is height
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js"></script>
<div style="width:300px;border:6px green solid;">
<table border="1" width="100%" id="tblNeedsScrolling">
<thead>
<tr><th>Header 1</th><th>Header 2</th></tr>
</thead>
<tbody>
<tr><td>row 1, cell 1</td><td>row 1, cell 2</td></tr>
<tr><td>row 2, cell 1</td><td>row 2, cell 2</td></tr>
<tr><td>row 3, cell 1</td><td>row 3, cell 2</td></tr>
<tr><td>row 4, cell 1</td><td>row 4, cell 2</td></tr>
<tr><td>row 5, cell 1</td><td>row 5, cell 2</td></tr>
<tr><td>row 6, cell 1</td><td>row 6, cell 2</td></tr>
<tr><td>row 7, cell 1</td><td>row 7, cell 2</td></tr>
<tr><td>row 8, cell 1</td><td>row 8, cell 2</td></tr>
</tbody>
</table>
</div>
This solution works in Chrome and IE. Since it is based on jQuery, this should work in other jQuery supported browsers as well.
I've just completed putting together a jQuery plugin that will take valid single table using valid HTML (have to have a thead and tbody) and will output a table that has fixed headers, optional fixed footer that can either be a cloned header or any content you chose (pagination, etc.). If you want to take advantage of larger monitors it will also resize the table when the browser is resized. Another added feature is being able to side scroll if the table columns can not all fit in view.
http://fixedheadertable.com/
on github: http://markmalek.github.com/Fixed-Header-Table/
It's extremely easy to setup and you can create your own custom styles for it. It also uses rounded corners in all browsers. Keep in mind I just released it, so it's still technically beta and there are very few minor issues I'm ironing out.
It works in Internet Explorer 7, Internet Explorer 8, Safari, Firefox and Chrome.
I also created a plugin that addresses this issue. My project - jQuery.floatThead has been around for over 4 years now and is very mature.
It requires no external styles and does not expect your table to be styled in any particular way. It supports Internet Explorer9+ and Firefox/Chrome.
Currently (2018-05) it has:
405 commits and 998 stars on GitHub
Many (not all) of the answers here are quick hacks that may have solved the problem one person was having, but will work not for every table.
Some of the other plugins are old and probably work great with Internet Explorer, but will break on Firefox and Chrome.
All of the attempts to solve this from outside the CSS specification are pale shadows of what we really want: Delivery on the implied promise of THEAD.
This frozen-headers-for-a-table issue has been an open wound in HTML/CSS for a long time.
In a perfect world, there would be a pure-CSS solution for this problem. Unfortunately, there doesn't seem to be a good one in place.
Relevant standards-discussions on this topic include:
The Sticky Positioning proposal at www-style: http://lists.w3.org/Archives/Public/www-style/2012Jun/0627.html
Tab Atkins' proposal for position-root, position-contain or position-restrict: http://www.xanthir.com/blog/b48H0
UPDATE: Firefox shipped position:sticky in version 32. Everyone wins!
TL;DR
If you target modern browsers and don't have extravagant styling needs: http://jsfiddle.net/dPixie/byB9d/3/ ... Although the big four version is pretty sweet as well this version handles fluid width a lot better.
Good news everyone!
With the advances of HTML5 and CSS3 this is now possible, at least for modern browsers. The slightly hackish implementation I came up with can be found here: http://jsfiddle.net/dPixie/byB9d/3/. I have tested it in FX 25, Chrome 31 and IE 10 ...
Relevant HTML (insert a HTML5 doctype at the top of your document though):
html,
body {
margin: 0;
padding: 0;
height: 100%;
}
section {
position: relative;
border: 1px solid #000;
padding-top: 37px;
background: #500;
}
section.positioned {
position: absolute;
top: 100px;
left: 100px;
width: 800px;
box-shadow: 0 0 15px #333;
}
.container {
overflow-y: auto;
height: 200px;
}
table {
border-spacing: 0;
width: 100%;
}
td+td {
border-left: 1px solid #eee;
}
td,
th {
border-bottom: 1px solid #eee;
background: #ddd;
color: #000;
padding: 10px 25px;
}
th {
height: 0;
line-height: 0;
padding-top: 0;
padding-bottom: 0;
color: transparent;
border: none;
white-space: nowrap;
}
th div {
position: absolute;
background: transparent;
color: #fff;
padding: 9px 25px;
top: 0;
margin-left: -25px;
line-height: normal;
border-left: 1px solid #800;
}
th:first-child div {
border: none;
}
<section class="positioned">
<div class="container">
<table>
<thead>
<tr class="header">
<th>
Table attribute name
<div>Table attribute name</div>
</th>
<th>
Value
<div>Value</div>
</th>
<th>
Description
<div>Description</div>
</th>
</tr>
</thead>
<tbody>
<tr>
<td>align</td>
<td>left, center, right</td>
<td>Not supported in HTML5. Deprecated in HTML 4.01. Specifies the alignment of a table according to surrounding text</td>
</tr>
<tr>
<td>bgcolor</td>
<td>rgb(x,x,x), #xxxxxx, colorname</td>
<td>Not supported in HTML5. Deprecated in HTML 4.01. Specifies the background color for a table</td>
</tr>
<tr>
<td>border</td>
<td>1,""</td>
<td>Specifies whether the table cells should have borders or not</td>
</tr>
<tr>
<td>cellpadding</td>
<td>pixels</td>
<td>Not supported in HTML5. Specifies the space between the cell wall and the cell content</td>
</tr>
<tr>
<td>cellspacing</td>
<td>pixels</td>
<td>Not supported in HTML5. Specifies the space between cells</td>
</tr>
<tr>
<td>frame</td>
<td>void, above, below, hsides, lhs, rhs, vsides, box, border</td>
<td>Not supported in HTML5. Specifies which parts of the outside borders that should be visible</td>
</tr>
<tr>
<td>rules</td>
<td>none, groups, rows, cols, all</td>
<td>Not supported in HTML5. Specifies which parts of the inside borders that should be visible</td>
</tr>
<tr>
<td>summary</td>
<td>text</td>
<td>Not supported in HTML5. Specifies a summary of the content of a table</td>
</tr>
<tr>
<td>width</td>
<td>pixels, %</td>
<td>Not supported in HTML5. Specifies the width of a table</td>
</tr>
</tbody>
</table>
</div>
</section>
But how?!
Simply put you have a table header, that you visually hide by making it 0px high, that also contains divs used as the fixed header. The table's container leaves enough room at the top to allow for the absolutely positioned header, and the table with scrollbars appear as you would expect.
The code above uses the positioned class to position the table absolutely (I'm using it in a popup style dialog) but you can use it in the flow of the document as well by removing the positioned class from the container.
But ...
It's not perfect. Firefox refuses to make the header row 0px (at least I did not find any way) but stubbornly keeps it at minimum 4px ... It's not a huge problem, but depending on your styling it will mess with your borders etc.
The table is also using a faux column approach where the background color of the container itself is used as the background for the header divs, that are transparent.
Summary
All in all there might be styling issues depending on your requirements, especially borders or complicated backgrounds. There might also be problems with computability, I haven't checked it in a wide variety of browsers yet (please comment with your experiences if you try it out), but I didn't find anything like it so I thought it was worth posting anyway ...
The CSS property position: sticky has great support in most modern browsers (I had issues with Edge, see below).
This lets us solve the problem of fixed headers quite easily:
thead th { position: sticky; top: 0; }
Safari needs a vendor prefix: -webkit-sticky.
For Firefox, I had to add min-height: 0 to one the parent elements. I forget exactly why this was needed.
Most unfortunately, the Microsoft Edge implementation seems to be only semi-working. At least, I had some flickering and misaligned table cells in my testing. The table was still usable, but had significant aesthetic issues.
Here is a jQuery plugin for fixed table headers. It allows the entire page to scroll, freezing the header when it reaches the top. It works well with Twitter Bootstrap tables.
GitHub repository: https://github.com/oma/table-fixed-header
It does not scroll only table content. Look to other tools for that, as one of these other answers. You decide what fits your case the best.
Most of the solutions posted here require jQuery. If you are looking for a framework independent solution try Grid: http://www.matts411.com/post/grid/
It's hosted on Github here: https://github.com/mmurph211/Grid
Not only does it support fixed headers, it also supports fixed left columns and footers, among other things.
A more refined pure CSS scrolling table
All of the pure CSS solutions I've seen so far-- clever though they may be-- lack a certain level of polish, or just don't work right in some situations. So, I decided to create my own...
Features:
It's pure CSS, so no jQuery required (or any JavaScript code at all, for that
matter)
You can set the table width to a percent (a.k.a. "fluid") or a fixed value, or let the content determine its width (a.k.a. "auto")
Column widths can also be fluid, fixed, or auto.
Columns will never become misaligned with headers due to horizontal scrolling (a problem that occurs in every other CSS-based solution I've seen that doesn't require fixed widths).
Compatible with all of the popular desktop browsers, including Internet Explorer back to version 8
Clean, polished appearance; no sloppy-looking 1-pixel gaps or misaligned borders; looks the same in all browsers
Here are a couple of fiddles that show the fluid and auto width options:
Fluid Width and Height (adapts to screen size): jsFiddle (Note that the scrollbar only shows up when needed in this configuration, so you may have to shrink the frame to see it)
Auto Width, Fixed Height (easier to integrate with other content): jsFiddle
The Auto Width, Fixed Height configuration probably has more use cases, so I'll post the code below.
/* The following 'html' and 'body' rule sets are required only
if using a % width or height*/
/*html {
width: 100%;
height: 100%;
}*/
body {
box-sizing: border-box;
width: 100%;
height: 100%;
margin: 0;
padding: 0 20px 0 20px;
text-align: center;
}
.scrollingtable {
box-sizing: border-box;
display: inline-block;
vertical-align: middle;
overflow: hidden;
width: auto; /* If you want a fixed width, set it here, else set to auto */
min-width: 0/*100%*/; /* If you want a % width, set it here, else set to 0 */
height: 188px/*100%*/; /* Set table height here; can be fixed value or % */
min-height: 0/*104px*/; /* If using % height, make this large enough to fit scrollbar arrows + caption + thead */
font-family: Verdana, Tahoma, sans-serif;
font-size: 16px;
line-height: 20px;
padding: 20px 0 20px 0; /* Need enough padding to make room for caption */
text-align: left;
color: black;
}
.scrollingtable * {box-sizing: border-box;}
.scrollingtable > div {
position: relative;
border-top: 1px solid black;
height: 100%;
padding-top: 20px; /* This determines column header height */
}
.scrollingtable > div:before {
top: 0;
background: cornflowerblue; /* Header row background color */
}
.scrollingtable > div:before,
.scrollingtable > div > div:after {
content: "";
position: absolute;
z-index: -1;
width: 100%;
height: 100%;
left: 0;
}
.scrollingtable > div > div {
min-height: 0/*43px*/; /* If using % height, make this large
enough to fit scrollbar arrows */
max-height: 100%;
overflow: scroll/*auto*/; /* Set to auto if using fixed
or % width; else scroll */
overflow-x: hidden;
border: 1px solid black; /* Border around table body */
}
.scrollingtable > div > div:after {background: white;} /* Match page background color */
.scrollingtable > div > div > table {
width: 100%;
border-spacing: 0;
margin-top: -20px; /* Inverse of column header height */
/*margin-right: 17px;*/ /* Uncomment if using % width */
}
.scrollingtable > div > div > table > caption {
position: absolute;
top: -20px; /*inverse of caption height*/
margin-top: -1px; /*inverse of border-width*/
width: 100%;
font-weight: bold;
text-align: center;
}
.scrollingtable > div > div > table > * > tr > * {padding: 0;}
.scrollingtable > div > div > table > thead {
vertical-align: bottom;
white-space: nowrap;
text-align: center;
}
.scrollingtable > div > div > table > thead > tr > * > div {
display: inline-block;
padding: 0 6px 0 6px; /*header cell padding*/
}
.scrollingtable > div > div > table > thead > tr > :first-child:before {
content: "";
position: absolute;
top: 0;
left: 0;
height: 20px; /*match column header height*/
border-left: 1px solid black; /*leftmost header border*/
}
.scrollingtable > div > div > table > thead > tr > * > div[label]:before,
.scrollingtable > div > div > table > thead > tr > * > div > div:first-child,
.scrollingtable > div > div > table > thead > tr > * + :before {
position: absolute;
top: 0;
white-space: pre-wrap;
color: white; /*header row font color*/
}
.scrollingtable > div > div > table > thead > tr > * > div[label]:before,
.scrollingtable > div > div > table > thead > tr > * > div[label]:after {content: attr(label);}
.scrollingtable > div > div > table > thead > tr > * + :before {
content: "";
display: block;
min-height: 20px; /* Match column header height */
padding-top: 1px;
border-left: 1px solid black; /* Borders between header cells */
}
.scrollingtable .scrollbarhead {float: right;}
.scrollingtable .scrollbarhead:before {
position: absolute;
width: 100px;
top: -1px; /* Inverse border-width */
background: white; /* Match page background color */
}
.scrollingtable > div > div > table > tbody > tr:after {
content: "";
display: table-cell;
position: relative;
padding: 0;
border-top: 1px solid black;
top: -1px; /* Inverse of border width */
}
.scrollingtable > div > div > table > tbody {vertical-align: top;}
.scrollingtable > div > div > table > tbody > tr {background: white;}
.scrollingtable > div > div > table > tbody > tr > * {
border-bottom: 1px solid black;
padding: 0 6px 0 6px;
height: 20px; /* Match column header height */
}
.scrollingtable > div > div > table > tbody:last-of-type > tr:last-child > * {border-bottom: none;}
.scrollingtable > div > div > table > tbody > tr:nth-child(even) {background: gainsboro;} /* Alternate row color */
.scrollingtable > div > div > table > tbody > tr > * + * {border-left: 1px solid black;} /* Borders between body cells */
<div class="scrollingtable">
<div>
<div>
<table>
<caption>Top Caption</caption>
<thead>
<tr>
<th><div label="Column 1"/></th>
<th><div label="Column 2"/></th>
<th><div label="Column 3"/></th>
<th>
<!-- More versatile way of doing column label; requires two identical copies of label -->
<div><div>Column 4</div><div>Column 4</div></div>
</th>
<th class="scrollbarhead"/> <!-- ALWAYS ADD THIS EXTRA CELL AT END OF HEADER ROW -->
</tr>
</thead>
<tbody>
<tr><td>Lorem ipsum</td><td>Dolor</td><td>Sit</td><td>Amet consectetur</td></tr>
<tr><td>Lorem ipsum</td><td>Dolor</td><td>Sit</td><td>Amet consectetur</td></tr>
<tr><td>Lorem ipsum</td><td>Dolor</td><td>Sit</td><td>Amet consectetur</td></tr>
<tr><td>Lorem ipsum</td><td>Dolor</td><td>Sit</td><td>Amet consectetur</td></tr>
<tr><td>Lorem ipsum</td><td>Dolor</td><td>Sit</td><td>Amet consectetur</td></tr>
<tr><td>Lorem ipsum</td><td>Dolor</td><td>Sit</td><td>Amet consectetur</td></tr>
<tr><td>Lorem ipsum</td><td>Dolor</td><td>Sit</td><td>Amet consectetur</td></tr>
<tr><td>Lorem ipsum</td><td>Dolor</td><td>Sit</td><td>Amet consectetur</td></tr>
<tr><td>Lorem ipsum</td><td>Dolor</td><td>Sit</td><td>Amet consectetur</td></tr>
<tr><td>Lorem ipsum</td><td>Dolor</td><td>Sit</td><td>Amet consectetur</td></tr>
<tr><td>Lorem ipsum</td><td>Dolor</td><td>Sit</td><td>Amet consectetur</td></tr>
<tr><td>Lorem ipsum</td><td>Dolor</td><td>Sit</td><td>Amet consectetur</td></tr>
</tbody>
</table>
</div>
Faux bottom caption
</div>
</div>
<!--[if lte IE 9]><style>.scrollingtable > div > div > table {margin-right: 17px;}</style><![endif]-->
The method I used to freeze the header row is similar to d-Pixie's, so refer to his post for an explanation. There were a slew of bugs and limitations with that technique that could only be fixed with heaps of additional CSS and an extra div container or two.
A simple jQuery plugin
This is a variation on Mahes' solution. You can call it like $('table#foo').scrollableTable();
The idea is:
Split the thead and tbody into separate table elements
Make their cell widths match again
Wrap the second table in a div.scrollable
Use CSS to make div.scrollable actually scroll
The CSS could be:
div.scrollable { height: 300px; overflow-y: scroll;}
Caveats
Obviously, splitting up these tables makes the markup less semantic. I'm not sure what effect this has on accessibility.
This plugin does not deal with footers, multiple headers, etc.
I've only tested it in Chrome version 20.
That said, it works for my purposes and you're free to take and modify it.
Here's the plugin:
jQuery.fn.scrollableTable = function () {
var $newTable, $oldTable, $scrollableDiv, originalWidths;
$oldTable = $(this);
// Once the tables are split, their cell widths may change.
// Grab these so we can make the two tables match again.
originalWidths = $oldTable.find('tr:first td').map(function() {
return $(this).width();
});
$newTable = $oldTable.clone();
$oldTable.find('tbody').remove();
$newTable.find('thead').remove();
$.each([$oldTable, $newTable], function(index, $table) {
$table.find('tr:first td').each(function(i) {
$(this).width(originalWidths[i]);
});
});
$scrollableDiv = $('<div/>').addClass('scrollable');
$newTable.insertAfter($oldTable).wrap($scrollableDiv);
};
:)
Not-so-clean, but pure HTML/CSS solution.
table {
overflow-x:scroll;
}
tbody {
max-height: /*your desired max height*/
overflow-y:scroll;
display:block;
}
Updated for IE8+
JSFiddle example
Somehow I ended up with Position:Sticky working fine on my case:
table{
width: 100%;
border: collapse;
}
th{
position: sticky;
top: 0px;
border: 1px solid black;
background: #ff5722;
color: #f5f5f5;
font-weight: 600;
}
td{
background: #d3d3d3;
border: 1px solid black;
color: #f5f5f5;
font-weight: 600;
}
div{
height: 150px
overflow: auto;
width: 100%
}
<div>
<table>
<thead>
<tr>
<th>header 1</th>
<th>header 2</th>
<th>header 3</th>
<th>header 4</th>
<th>header 5</th>
<th>header 6</th>
<th>header 7</th>
</tr>
</thead>
<tbody>
<tr>
<td>data 1</td>
<td>data 2</td>
<td>data 3</td>
<td>data 4</td>
<td>data 5</td>
<td>data 6</td>
<td>data 7</td>
</tr>
<tr>
<td>data 1</td>
<td>data 2</td>
<td>data 3</td>
<td>data 4</td>
<td>data 5</td>
<td>data 6</td>
<td>data 7</td>
</tr>
<tr>
<td>data 1</td>
<td>data 2</td>
<td>data 3</td>
<td>data 4</td>
<td>data 5</td>
<td>data 6</td>
<td>data 7</td>
</tr>
<tr>
<td>data 1</td>
<td>data 2</td>
<td>data 3</td>
<td>data 4</td>
<td>data 5</td>
<td>data 6</td>
<td>data 7</td>
</tr>
<tr>
<td>data 1</td>
<td>data 2</td>
<td>data 3</td>
<td>data 4</td>
<td>data 5</td>
<td>data 6</td>
<td>data 7</td>
</tr>
<tr>
<td>data 1</td>
<td>data 2</td>
<td>data 3</td>
<td>data 4</td>
<td>data 5</td>
<td>data 6</td>
<td>data 7</td>
</tr>
<tr>
<td>data 1</td>
<td>data 2</td>
<td>data 3</td>
<td>data 4</td>
<td>data 5</td>
<td>data 6</td>
<td>data 7</td>
</tr>
<tr>
<td>data 1</td>
<td>data 2</td>
<td>data 3</td>
<td>data 4</td>
<td>data 5</td>
<td>data 6</td>
<td>data 7</td>
</tr>
<tr>
<td>data 1</td>
<td>data 2</td>
<td>data 3</td>
<td>data 4</td>
<td>data 5</td>
<td>data 6</td>
<td>data 7</td>
</tr>
<tr>
<td>data 1</td>
<td>data 2</td>
<td>data 3</td>
<td>data 4</td>
<td>data 5</td>
<td>data 6</td>
<td>data 7</td>
</tr>
<tr>
<td>data 1</td>
<td>data 2</td>
<td>data 3</td>
<td>data 4</td>
<td>data 5</td>
<td>data 6</td>
<td>data 7</td>
</tr>
<tr>
<td>data 1</td>
<td>data 2</td>
<td>data 3</td>
<td>data 4</td>
<td>data 5</td>
<td>data 6</td>
<td>data 7</td>
</tr>
<tr>
<td>data 1</td>
<td>data 2</td>
<td>data 3</td>
<td>data 4</td>
<td>data 5</td>
<td>data 6</td>
<td>data 7</td>
</tr>
<tr>
<td>data 1</td>
<td>data 2</td>
<td>data 3</td>
<td>data 4</td>
<td>data 5</td>
<td>data 6</td>
<td>data 7</td>
</tr>
</tbody>
</table>
</div>
Support for fixed footer
I extended Nathan's function to also support a fixed footer and maximum height.
Also, the function will set the CSS itself, and you only have to support a width.
Usage:
Fixed height:
$('table').scrollableTable({ height: 100 });
Maximum height (if the browser supports the CSS 'max-height' option):
$('table').scrollableTable({ maxHeight: 100 });
Script:
jQuery.fn.scrollableTable = function(options) {
var $originalTable, $headTable, $bodyTable, $footTable, $scrollableDiv, originalWidths;
// Prepare the separate parts of the table
$originalTable = $(this);
$headTable = $originalTable.clone();
$headTable.find('tbody').remove();
$headTable.find('tfoot').remove();
$bodyTable = $originalTable.clone();
$bodyTable.find('thead').remove();
$bodyTable.find('tfoot').remove();
$footTable = $originalTable.clone();
$footTable.find('thead').remove();
$footTable.find('tbody').remove();
// Grab the original column widths and set them in the separate tables
originalWidths = $originalTable.find('tr:first td').map(function() {
return $(this).width();
});
$.each([$headTable, $bodyTable, $footTable], function(index, $table) {
$table.find('tr:first td').each(function(i) {
$(this).width(originalWidths[i]);
});
});
// The div that makes the body table scroll
$scrollableDiv = $('<div/>').css({
'overflow-y': 'scroll'
});
if(options.height) {
$scrollableDiv.css({'height': options.height});
}
else if(options.maxHeight) {
$scrollableDiv.css({'max-height': options.maxHeight});
}
// Add the new separate tables and remove the original one
$headTable.insertAfter($originalTable);
$bodyTable.insertAfter($headTable);
$footTable.insertAfter($bodyTable);
$bodyTable.wrap($scrollableDiv);
$originalTable.remove();
};
Two divs, one for header, one for data. Make the data div scrollable, and use JavaScript to set the width of the columns in the header to be the same as the widths in the data. I think the data columns widths need to be fixed rather than dynamic.
I found this workaround - move header row in a table above table with data:
<html>
<head>
<title>Fixed header</title>
<style>
table td {width:75px;}
</style>
</head>
<body>
<div style="height:auto; width:350px; overflow:auto">
<table border="1">
<tr>
<td>header 1</td>
<td>header 2</td>
<td>header 3</td>
</tr>
</table>
</div>
<div style="height:50px; width:350px; overflow:auto">
<table border="1">
<tr>
<td>row 1 col 1</td>
<td>row 1 col 2</td>
<td>row 1 col 3</td>
</tr>
<tr>
<td>row 2 col 1</td>
<td>row 2 col 2</td>
<td>row 2 col 3</td>
</tr>
<tr>
<td>row 3 col 1</td>
<td>row 3 col 2</td>
<td>row 3 col 3</td>
</tr>
<tr>
<td>row 4 col 1</td>
<td>row 4 col 2</td>
<td>row 4 col 3</td>
</tr>
<tr>
<td>row 5 col 1</td>
<td>row 5 col 2</td>
<td>row 5 col 3</td>
</tr>
<tr>
<td>row 6 col 1</td>
<td>row 6 col 2</td>
<td>row 6 col 3</td>
</tr>
</table>
</div>
</body>
</html>
I realize the question allows JavaScript, but here is a pure CSS solution I worked up that also allows for the table to expand horizontally. It was tested with Internet Explorer 10 and the latest Chrome and Firefox browsers. A link to jsFiddle is at the bottom.
The HTML:
Putting some text here to differentiate between the header
aligning with the top of the screen and the header aligning
with the top of one of its ancestor containers.
<div id="positioning-container">
<div id="scroll-container">
<table>
<colgroup>
<col class="col1"></col>
<col class="col2"></col>
</colgroup>
<thead>
<th class="header-col1"><div>Header 1</div></th>
<th class="header-col2"><div>Header 2</div></th>
</thead>
<tbody>
<tr><td>Cell 1.1</td><td>Cell 1.2</td></tr>
<tr><td>Cell 2.1</td><td>Cell 2.2</td></tr>
<tr><td>Cell 3.1</td><td>Cell 3.2</td></tr>
<tr><td>Cell 4.1</td><td>Cell 4.2</td></tr>
<tr><td>Cell 5.1</td><td>Cell 5.2</td></tr>
<tr><td>Cell 6.1</td><td>Cell 6.2</td></tr>
<tr><td>Cell 7.1</td><td>Cell 7.2</td></tr>
</tbody>
</table>
</div>
</div>
And the CSS:
table{
border-collapse: collapse;
table-layout: fixed;
width: 100%;
}
/* Not required, just helps with alignment for this example */
td, th{
padding: 0;
margin: 0;
}
tbody{
background-color: #ddf;
}
thead {
/* Keeps the header in place. Don't forget top: 0 */
position: absolute;
top: 0;
background-color: #ddd;
/* The 17px is to adjust for the scrollbar width.
* This is a new css value that makes this pure
* css example possible */
width: calc(100% - 17px);
height: 20px;
}
/* Positioning container. Required to position the
* header since the header uses position:absolute
* (otherwise it would position at the top of the screen) */
#positioning-container{
position: relative;
}
/* A container to set the scroll-bar and
* includes padding to move the table contents
* down below the header (padding = header height) */
#scroll-container{
overflow-y: auto;
padding-top: 20px;
height: 100px;
}
.header-col1{
background-color: red;
}
/* Fixed-width header columns need a div to set their width */
.header-col1 div{
width: 100px;
}
/* Expandable columns need a width set on the th tag */
.header-col2{
width: 100%;
}
.col1 {
width: 100px;
}
.col2{
width: 100%;
}
http://jsfiddle.net/HNHRv/3/
For those who tried the nice solution given by Maximilian Hils, and did not succeed to get it to work with Internet Explorer, I had the same problem (Internet Explorer 11) and found out what was the problem.
In Internet Explorer 11 the style transform (at least with translate) does not work on <THEAD>. I solved this by instead applying the style to all the <TH> in a loop. That worked. My JavaScript code looks like this:
document.getElementById('pnlGridWrap').addEventListener("scroll", function () {
var translate = "translate(0," + this.scrollTop + "px)";
var myElements = this.querySelectorAll("th");
for (var i = 0; i < myElements.length; i++) {
myElements[i].style.transform=translate;
}
});
In my case the table was a GridView in ASP.NET. First I thought it was because it had no <THEAD>, but even when I forced it to have one, it did not work. Then I found out what I wrote above.
It is a very nice and simple solution. On Chrome it is perfect, on Firefox a bit jerky, and on Internet Explorer even more jerky. But all in all a good solution.
Very late to the party, but as it's still a party, here's my two cents using tailwindcss:
<div class="h-screen overflow-hidden flex flex-col">
<div class="overflow-y-scroll flex-1">
<table>
<thead class="sticky top-0">
<tr>
<th>Timestamp</th>
<th>Species</th>
</tr>
</thead>
<tbody>
<tr>
<td>2022-02-09T08:20:39.967Z</td>
<td>willow</td>
</tr>
<tr>
<td>2022-02-09T08:21:29.453Z</td>
<td>red osier dogwood</td>
</tr>
<tr>
<td>2022-02-09T08:22:18.984Z</td>
<td>buttonbush</td>
</tr>
</tbody>
</table>
</div>
</div>
Here's a full working example on tailwind's playgroud.
I wish I had found #Mark's solution earlier, but I went and wrote my own before I saw this SO question...
Mine is a very lightweight jQuery plugin that supports fixed header, footer, column spanning (colspan), resizing, horizontal scrolling, and an optional number of rows to display before scrolling starts.
jQuery.scrollTableBody (GitHub)
As long as you have a table with proper <thead>, <tbody>, and (optional) <tfoot>, all you need to do is this:
$('table').scrollTableBody();
By applying the StickyTableHeaders jQuery plugin to the table, the column headers will stick to the top of the viewport as you scroll down.
Example:
$(function () {
$("table").stickyTableHeaders();
});
/*! Copyright (c) 2011 by Jonas Mosbech - https://github.com/jmosbech/StickyTableHeaders
MIT license info: https://github.com/jmosbech/StickyTableHeaders/blob/master/license.txt */
;
(function ($, window, undefined) {
'use strict';
var name = 'stickyTableHeaders',
id = 0,
defaults = {
fixedOffset: 0,
leftOffset: 0,
marginTop: 0,
scrollableArea: window
};
function Plugin(el, options) {
// To avoid scope issues, use 'base' instead of 'this'
// to reference this class from internal events and functions.
var base = this;
// Access to jQuery and DOM versions of element
base.$el = $(el);
base.el = el;
base.id = id++;
base.$window = $(window);
base.$document = $(document);
// Listen for destroyed, call teardown
base.$el.bind('destroyed',
$.proxy(base.teardown, base));
// Cache DOM refs for performance reasons
base.$clonedHeader = null;
base.$originalHeader = null;
// Keep track of state
base.isSticky = false;
base.hasBeenSticky = false;
base.leftOffset = null;
base.topOffset = null;
base.init = function () {
base.$el.each(function () {
var $this = $(this);
// remove padding on <table> to fix issue #7
$this.css('padding', 0);
base.$originalHeader = $('thead:first', this);
base.$clonedHeader = base.$originalHeader.clone();
$this.trigger('clonedHeader.' + name, [base.$clonedHeader]);
base.$clonedHeader.addClass('tableFloatingHeader');
base.$clonedHeader.css('display', 'none');
base.$originalHeader.addClass('tableFloatingHeaderOriginal');
base.$originalHeader.after(base.$clonedHeader);
base.$printStyle = $('<style type="text/css" media="print">' +
'.tableFloatingHeader{display:none !important;}' +
'.tableFloatingHeaderOriginal{position:static !important;}' +
'</style>');
$('head').append(base.$printStyle);
});
base.setOptions(options);
base.updateWidth();
base.toggleHeaders();
base.bind();
};
base.destroy = function () {
base.$el.unbind('destroyed', base.teardown);
base.teardown();
};
base.teardown = function () {
if (base.isSticky) {
base.$originalHeader.css('position', 'static');
}
$.removeData(base.el, 'plugin_' + name);
base.unbind();
base.$clonedHeader.remove();
base.$originalHeader.removeClass('tableFloatingHeaderOriginal');
base.$originalHeader.css('visibility', 'visible');
base.$printStyle.remove();
base.el = null;
base.$el = null;
};
base.bind = function () {
base.$scrollableArea.on('scroll.' + name, base.toggleHeaders);
if (!base.isWindowScrolling) {
base.$window.on('scroll.' + name + base.id, base.setPositionValues);
base.$window.on('resize.' + name + base.id, base.toggleHeaders);
}
base.$scrollableArea.on('resize.' + name, base.toggleHeaders);
base.$scrollableArea.on('resize.' + name, base.updateWidth);
};
base.unbind = function () {
// unbind window events by specifying handle so we don't remove too much
base.$scrollableArea.off('.' + name, base.toggleHeaders);
if (!base.isWindowScrolling) {
base.$window.off('.' + name + base.id, base.setPositionValues);
base.$window.off('.' + name + base.id, base.toggleHeaders);
}
base.$scrollableArea.off('.' + name, base.updateWidth);
};
base.toggleHeaders = function () {
if (base.$el) {
base.$el.each(function () {
var $this = $(this),
newLeft,
newTopOffset = base.isWindowScrolling ? (
isNaN(base.options.fixedOffset) ? base.options.fixedOffset.outerHeight() : base.options.fixedOffset) : base.$scrollableArea.offset().top + (!isNaN(base.options.fixedOffset) ? base.options.fixedOffset : 0),
offset = $this.offset(),
scrollTop = base.$scrollableArea.scrollTop() + newTopOffset,
scrollLeft = base.$scrollableArea.scrollLeft(),
scrolledPastTop = base.isWindowScrolling ? scrollTop > offset.top : newTopOffset > offset.top,
notScrolledPastBottom = (base.isWindowScrolling ? scrollTop : 0) < (offset.top + $this.height() - base.$clonedHeader.height() - (base.isWindowScrolling ? 0 : newTopOffset));
if (scrolledPastTop && notScrolledPastBottom) {
newLeft = offset.left - scrollLeft + base.options.leftOffset;
base.$originalHeader.css({
'position': 'fixed',
'margin-top': base.options.marginTop,
'left': newLeft,
'z-index': 3 // #18: opacity bug
});
base.leftOffset = newLeft;
base.topOffset = newTopOffset;
base.$clonedHeader.css('display', '');
if (!base.isSticky) {
base.isSticky = true;
// make sure the width is correct: the user might have resized the browser while in static mode
base.updateWidth();
}
base.setPositionValues();
} else if (base.isSticky) {
base.$originalHeader.css('position', 'static');
base.$clonedHeader.css('display', 'none');
base.isSticky = false;
base.resetWidth($('td,th', base.$clonedHeader), $('td,th', base.$originalHeader));
}
});
}
};
base.setPositionValues = function () {
var winScrollTop = base.$window.scrollTop(),
winScrollLeft = base.$window.scrollLeft();
if (!base.isSticky || winScrollTop < 0 || winScrollTop + base.$window.height() > base.$document.height() || winScrollLeft < 0 || winScrollLeft + base.$window.width() > base.$document.width()) {
return;
}
base.$originalHeader.css({
'top': base.topOffset - (base.isWindowScrolling ? 0 : winScrollTop),
'left': base.leftOffset - (base.isWindowScrolling ? 0 : winScrollLeft)
});
};
base.updateWidth = function () {
if (!base.isSticky) {
return;
}
// Copy cell widths from clone
if (!base.$originalHeaderCells) {
base.$originalHeaderCells = $('th,td', base.$originalHeader);
}
if (!base.$clonedHeaderCells) {
base.$clonedHeaderCells = $('th,td', base.$clonedHeader);
}
var cellWidths = base.getWidth(base.$clonedHeaderCells);
base.setWidth(cellWidths, base.$clonedHeaderCells, base.$originalHeaderCells);
// Copy row width from whole table
base.$originalHeader.css('width', base.$clonedHeader.width());
};
base.getWidth = function ($clonedHeaders) {
var widths = [];
$clonedHeaders.each(function (index) {
var width, $this = $(this);
if ($this.css('box-sizing') === 'border-box') {
width = $this[0].getBoundingClientRect().width; // #39: border-box bug
} else {
var $origTh = $('th', base.$originalHeader);
if ($origTh.css('border-collapse') === 'collapse') {
if (window.getComputedStyle) {
width = parseFloat(window.getComputedStyle(this, null).width);
} else {
// ie8 only
var leftPadding = parseFloat($this.css('padding-left'));
var rightPadding = parseFloat($this.css('padding-right'));
// Needs more investigation - this is assuming constant border around this cell and it's neighbours.
var border = parseFloat($this.css('border-width'));
width = $this.outerWidth() - leftPadding - rightPadding - border;
}
} else {
width = $this.width();
}
}
widths[index] = width;
});
return widths;
};
base.setWidth = function (widths, $clonedHeaders, $origHeaders) {
$clonedHeaders.each(function (index) {
var width = widths[index];
$origHeaders.eq(index).css({
'min-width': width,
'max-width': width
});
});
};
base.resetWidth = function ($clonedHeaders, $origHeaders) {
$clonedHeaders.each(function (index) {
var $this = $(this);
$origHeaders.eq(index).css({
'min-width': $this.css('min-width'),
'max-width': $this.css('max-width')
});
});
};
base.setOptions = function (options) {
base.options = $.extend({}, defaults, options);
base.$scrollableArea = $(base.options.scrollableArea);
base.isWindowScrolling = base.$scrollableArea[0] === window;
};
base.updateOptions = function (options) {
base.setOptions(options);
// scrollableArea might have changed
base.unbind();
base.bind();
base.updateWidth();
base.toggleHeaders();
};
// Run initializer
base.init();
}
// A plugin wrapper around the constructor,
// preventing against multiple instantiations
$.fn[name] = function (options) {
return this.each(function () {
var instance = $.data(this, 'plugin_' + name);
if (instance) {
if (typeof options === 'string') {
instance[options].apply(instance);
} else {
instance.updateOptions(options);
}
} else if (options !== 'destroy') {
$.data(this, 'plugin_' + name, new Plugin(this, options));
}
});
};
})(jQuery, window);
body {
margin: 0 auto;
padding: 0 20px;
font-family: Arial, Helvetica, sans-serif;
font-size: 11px;
color: #555;
}
table {
border: 0;
padding: 0;
margin: 0 0 20px 0;
border-collapse: collapse;
}
th {
padding: 5px;
/* NOTE: th padding must be set explicitly in order to support IE */
text-align: right;
font-weight:bold;
line-height: 2em;
color: #FFF;
background-color: #555;
}
tbody td {
padding: 10px;
line-height: 18px;
border-top: 1px solid #E0E0E0;
}
tbody tr:nth-child(2n) {
background-color: #F7F7F7;
}
tbody tr:hover {
background-color: #EEEEEE;
}
td {
text-align: right;
}
td:first-child, th:first-child {
text-align: left;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<div style="width:3000px">some really really wide content goes here</div>
<table>
<thead>
<tr>
<th colspan="9">Companies listed on NASDAQ OMX Copenhagen.</th>
</tr>
<tr>
<th>Full name</th>
<th>CCY</th>
<th>Last</th>
<th>+/-</th>
<th>%</th>
<th>Bid</th>
<th>Ask</th>
<th>Volume</th>
<th>Turnover</th>
</tr>
</thead>
<tbody>
<tr>
<td>A.P. Møller...</td>
<td>DKK</td>
<td>33,220.00</td>
<td>760</td>
<td>2.34</td>
<td>33,140.00</td>
<td>33,220.00</td>
<td>594</td>
<td>19,791,910</td>
</tr>
<tr>
<td>A.P. Møller...</td>
<td>DKK</td>
<td>34,620.00</td>
<td>640</td>
<td>1.88</td>
<td>34,620.00</td>
<td>34,700.00</td>
<td>9,954</td>
<td>346,530,246</td>
</tr>
<tr>
<td>Carlsberg A</td>
<td>DKK</td>
<td>380</td>
<td>0</td>
<td>0</td>
<td>371</td>
<td>391.5</td>
<td>6</td>
<td>2,280</td>
</tr>
<tr>
<td>Carlsberg B</td>
<td>DKK</td>
<td>364.4</td>
<td>8.6</td>
<td>2.42</td>
<td>363</td>
<td>364.4</td>
<td>636,267</td>
<td>228,530,601</td>
</tr>
<tr>
<td>Chr. Hansen...</td>
<td>DKK</td>
<td>114.5</td>
<td>-1.6</td>
<td>-1.38</td>
<td>114.2</td>
<td>114.5</td>
<td>141,822</td>
<td>16,311,454</td>
</tr>
<tr>
<td>Coloplast B</td>
<td>DKK</td>
<td>809.5</td>
<td>11</td>
<td>1.38</td>
<td>809</td>
<td>809.5</td>
<td>85,840</td>
<td>69,363,301</td>
</tr>
<tr>
<td>D/S Norden</td>
<td>DKK</td>
<td>155</td>
<td>-1.5</td>
<td>-0.96</td>
<td>155</td>
<td>155.1</td>
<td>51,681</td>
<td>8,037,225</td>
</tr>
<tr>
<td>Danske Bank</td>
<td>DKK</td>
<td>69.05</td>
<td>2.55</td>
<td>3.83</td>
<td>69.05</td>
<td>69.2</td>
<td>1,723,719</td>
<td>115,348,068</td>
</tr>
<tr>
<td>DSV</td>
<td>DKK</td>
<td>105.4</td>
<td>0.2</td>
<td>0.19</td>
<td>105.2</td>
<td>105.4</td>
<td>674,873</td>
<td>71,575,035</td>
</tr>
<tr>
<td>FLSmidth & Co.</td>
<td>DKK</td>
<td>295.8</td>
<td>-1.8</td>
<td>-0.6</td>
<td>295.1</td>
<td>295.8</td>
<td>341,263</td>
<td>100,301,032</td>
</tr>
<tr>
<td>G4S plc</td>
<td>DKK</td>
<td>22.53</td>
<td>0.05</td>
<td>0.22</td>
<td>22.53</td>
<td>22.57</td>
<td>190,920</td>
<td>4,338,150</td>
</tr>
<tr>
<td>Jyske Bank</td>
<td>DKK</td>
<td>144.2</td>
<td>1.4</td>
<td>0.98</td>
<td>142.8</td>
<td>144.2</td>
<td>78,163</td>
<td>11,104,874</td>
</tr>
<tr>
<td>Københavns ...</td>
<td>DKK</td>
<td>1,580.00</td>
<td>-12</td>
<td>-0.75</td>
<td>1,590.00</td>
<td>1,620.00</td>
<td>82</td>
<td>131,110</td>
</tr>
<tr>
<td>Lundbeck</td>
<td>DKK</td>
<td>103.4</td>
<td>-2.5</td>
<td>-2.36</td>
<td>103.4</td>
<td>103.8</td>
<td>157,162</td>
<td>16,462,282</td>
</tr>
<tr>
<td>Nordea Bank</td>
<td>DKK</td>
<td>43.22</td>
<td>-0.06</td>
<td>-0.14</td>
<td>43.22</td>
<td>43.25</td>
<td>167,520</td>
<td>7,310,143</td>
</tr>
<tr>
<td>Novo Nordisk B</td>
<td>DKK</td>
<td>552.5</td>
<td>-3.5</td>
<td>-0.63</td>
<td>550.5</td>
<td>552.5</td>
<td>843,533</td>
<td>463,962,375</td>
</tr>
<tr>
<td>Novozymes B</td>
<td>DKK</td>
<td>805.5</td>
<td>5.5</td>
<td>0.69</td>
<td>805</td>
<td>805.5</td>
<td>152,188</td>
<td>121,746,199</td>
</tr>
<tr>
<td>Pandora</td>
<td>DKK</td>
<td>39.04</td>
<td>0.94</td>
<td>2.47</td>
<td>38.8</td>
<td>39.04</td>
<td>350,965</td>
<td>13,611,838</td>
</tr>
<tr>
<td>Rockwool In...</td>
<td>DKK</td>
<td>492</td>
<td>0</td>
<td>0</td>
<td>482</td>
<td>492</td>
<td></td>
<td></td>
</tr>
<tr>
<td>Rockwool In...</td>
<td>DKK</td>
<td>468</td>
<td>12</td>
<td>2.63</td>
<td>465.2</td>
<td>468</td>
<td>9,885</td>
<td>4,623,850</td>
</tr>
<tr>
<td>Sydbank</td>
<td>DKK</td>
<td>95</td>
<td>0.05</td>
<td>0.05</td>
<td>94.7</td>
<td>95</td>
<td>103,438</td>
<td>9,802,899</td>
</tr>
<tr>
<td>TDC</td>
<td>DKK</td>
<td>43.6</td>
<td>0.13</td>
<td>0.3</td>
<td>43.5</td>
<td>43.6</td>
<td>845,110</td>
<td>36,785,339</td>
</tr>
<tr>
<td>Topdanmark</td>
<td>DKK</td>
<td>854</td>
<td>13.5</td>
<td>1.61</td>
<td>854</td>
<td>855</td>
<td>38,679</td>
<td>32,737,678</td>
</tr>
<tr>
<td>Tryg</td>
<td>DKK</td>
<td>290.4</td>
<td>0.3</td>
<td>0.1</td>
<td>290</td>
<td>290.4</td>
<td>94,587</td>
<td>27,537,247</td>
</tr>
<tr>
<td>Vestas Wind...</td>
<td>DKK</td>
<td>90.15</td>
<td>-4.2</td>
<td>-4.45</td>
<td>90.1</td>
<td>90.15</td>
<td>1,317,313</td>
<td>121,064,314</td>
</tr>
<tr>
<td>William Dem...</td>
<td>DKK</td>
<td>417.6</td>
<td>0.1</td>
<td>0.02</td>
<td>417</td>
<td>417.6</td>
<td>64,242</td>
<td>26,859,554</td>
</tr>
</tbody>
</table>
<div style="height: 4000px">lots of content down here...</div>
I like Maximillian Hils' answer but I had a some issues:
the transform doesn't work in Edge or IE unless you apply it to the th
the header flickers during scrolling in Edge and IE
my table is loaded using ajax, so I wanted to attach to the window scroll event rather than the wrapper's scroll event
To get rid of the flicker, I use a timeout to wait until the user has finished scrolling, then I apply the transform - so the header is not visible during scrolling.
I have also written this using jQuery, one advantage of that being that jQuery should handle vendor-prefixes for you
var isScrolling, lastTop, lastLeft, isLeftHidden, isTopHidden;
//Scroll events don't bubble https://stackoverflow.com/a/19375645/150342
//so can't use $(document).on("scroll", ".table-container-fixed", function (e) {
document.addEventListener('scroll', function (event) {
var $container = $(event.target);
if (!$container.hasClass("table-container-fixed"))
return;
//transform needs to be applied to th for Edge and IE
//in this example I am also fixing the leftmost column
var $topLeftCell = $container.find('table:first > thead > tr > th:first');
var $headerCells = $topLeftCell.siblings();
var $columnCells = $container
.find('table:first > tbody > tr > td:first-child, ' +
'table:first > tfoot > tr > td:first-child');
//hide the cells while returning otherwise they show on top of the data
if (!isLeftHidden) {
var currentLeft = $container.scrollLeft();
if (currentLeft < lastLeft) {
//scrolling left
isLeftHidden = true;
$topLeftCell.css('visibility', 'hidden');
$columnCells.css('visibility', 'hidden');
}
lastLeft = currentLeft;
}
if (!isTopHidden) {
var currentTop = $container.scrollTop();
if (currentTop < lastTop) {
//scrolling up
isTopHidden = true;
$topLeftCell.css('visibility', 'hidden');
$headerCells.css('visibility', 'hidden');
}
lastTop = currentTop;
}
// Using timeout to delay transform until user stops scrolling
// Clear timeout while scrolling
window.clearTimeout(isScrolling);
// Set a timeout to run after scrolling ends
isScrolling = setTimeout(function () {
//move the table cells.
var x = $container.scrollLeft();
var y = $container.scrollTop();
$topLeftCell.css('transform', 'translate(' + x + 'px, ' + y + 'px)');
$headerCells.css('transform', 'translateY(' + y + 'px)');
$columnCells.css('transform', 'translateX(' + x + 'px)');
isTopHidden = isLeftHidden = false;
$topLeftCell.css('visibility', 'inherit');
$headerCells.css('visibility', 'inherit');
$columnCells.css('visibility', 'inherit');
}, 100);
}, true);
The table is wrapped in a div with the class table-container-fixed.
.table-container-fixed{
overflow: auto;
height: 400px;
}
I set border-collapse to separate because otherwise we lose borders during translation, and I remove the border on the table to stop content appearing just above the cell where the border was during scrolling.
.table-container-fixed > table {
border-collapse: separate;
border:none;
}
I make the th background white to cover the cells underneath, and I add a border that matches the table border - which is styled using Bootstrap and scrolled out of view.
.table-container-fixed > table > thead > tr > th {
border-top: 1px solid #ddd !important;
background-color: white;
z-index: 10;
position: relative;/*to make z-index work*/
}
.table-container-fixed > table > thead > tr > th:first-child {
z-index: 20;
}
.table-container-fixed > table > tbody > tr > td:first-child,
.table-container-fixed > table > tfoot > tr > td:first-child {
background-color: white;
z-index: 10;
position: relative;
}
Use the latest version of jQuery, and include the following JavaScript code.
$(window).scroll(function(){
$("id of the div element").offset({top:$(window).scrollTop()});
});
This is not an exact solution to the fixed header row, but I have created a rather ingenious method of repeating the header row throughout the long table, yet still keeping the ability to sort.
This neat little option requires the jQuery tablesorter plugin. Here's how it works:
HTML
<table class="tablesorter boxlist" id="pmtable">
<thead class="fixedheader">
<tr class="boxheadrow">
<th width="70px" class="header">Job Number</th>
<th width="10px" class="header">Pri</th>
<th width="70px" class="header">CLLI</th>
<th width="35px" class="header">Market</th>
<th width="35px" class="header">Job Status</th>
<th width="65px" class="header">Technology</th>
<th width="95px;" class="header headerSortDown">MEI</th>
<th width="95px" class="header">TEO Writer</th>
<th width="75px" class="header">Quote Due</th>
<th width="100px" class="header">Engineer</th>
<th width="75px" class="header">ML Due</th>
<th width="75px" class="header">ML Complete</th>
<th width="75px" class="header">SPEC Due</th>
<th width="75px" class="header">SPEC Complete</th>
<th width="100px" class="header">Install Supervisor</th>
<th width="75px" class="header">MasTec OJD</th>
<th width="75px" class="header">Install Start</th>
<th width="30px" class="header">Install Hours</th>
<th width="75px" class="header">Revised CRCD</th>
<th width="75px" class="header">Latest Ship-To-Site</th>
<th width="30px" class="header">Total Parts</th>
<th width="30px" class="header">OEM Rcvd</th>
<th width="30px" class="header">Minor Rcvd</th>
<th width="30px" class="header">Total Received</th>
<th width="30px" class="header">% On Site</th>
<th width="60px" class="header">Actions</th>
</tr>
</thead>
<tbody class="scrollable">
<tr data-job_id="3548" data-ml_id="" class="odd">
<td class="c black">FL-8-RG9UP</td>
<td data-pri="2" class="priority c yellow">M</td>
<td class="c">FTLDFLOV</td>
<td class="c">SFL</td>
<td class="c">NOI</td>
<td class="c">TRANSPORT</td>
<td class="c"></td>
<td class="c">Chris Byrd</td>
<td class="c">Apr 13, 2013</td>
<td class="c">Kris Hall</td>
<td class="c">May 20, 2013</td>
<td class="c">May 20, 2013</td>
<td class="c">Jun 5, 2013</td>
<td class="c">Jun 7, 2013</td>
<td class="c">Joseph Fitz</td>
<td class="c">Jun 10, 2013</td>
<td class="c">TBD</td>
<td class="c">123</td>
<td class="c revised_crcd"><input readonly="true" name="revised_crcd" value="Jul 26, 2013" type="text" size="12" class="smInput r_crcd c hasDatepicker" id="dp1377194058616"></td>
<td class="c">TBD</td>
<td class="c">N/A</td>
<td class="c">N/A</td>
<td class="c">N/A</td>
<td class="c">N/A</td>
<td class="c">N/A</td>
<td class="actions"><span style="float:left;" class="ui-icon ui-icon-folder-open editJob" title="View this job" s="" details'=""></span></td>
</tr>
<tr data-job_id="4264" data-ml_id="2959" class="even">
<td class="c black">MTS13009SF</td>
<td data-pri="2" class="priority c yellow">M</td>
<td class="c">OJUSFLTL</td>
<td class="c">SFL</td>
<td class="c">NOI</td>
<td class="c">TRANSPORT</td>
<td class="c"></td>
<td class="c">DeMarcus Stewart</td>
<td class="c">May 22, 2013</td>
<td class="c">Ryan Alsobrook</td>
<td class="c">Jun 19, 2013</td>
<td class="c">Jun 27, 2013</td>
<td class="c">Jun 19, 2013</td>
<td class="c">Jul 4, 2013</td>
<td class="c">Randy Williams</td>
<td class="c">Jun 21, 2013</td>
<td class="c">TBD</td>
<td class="c">95</td>
<td class="c revised_crcd"><input readonly="true" name="revised_crcd" value="Aug 9, 2013" type="text" size="12" class="smInput r_crcd c hasDatepicker" id="dp1377194058632"></td><td class="c">TBD</td>
<td class="c">0</td>
<td class="c">0.00%</td>
<td class="c">0.00%</td>
<td class="c">0.00%</td>
<td class="c">0.00%</td>
<td class="actions"><span style="float:left;" class="ui-icon ui-icon-folder-open editJob" title="View this job" s="" details'=""></span><input style="float:left;" type="hidden" name="req_ship" class="reqShip hasDatepicker" id="dp1377194058464"><span style="float:left;" class="ui-icon ui-icon-calendar requestShip" title="Schedule this job for shipping"></span><span class="ui-icon ui-icon-info viewOrderInfo" style="float:left;" title="Show material details for this order"></span></td>
</tr>
.
.
.
.
<tr class="boxheadrow repeated-header">
<th width="70px" class="header">Job Number</th>
<th width="10px" class="header">Pri</th>
<th width="70px" class="header">CLLI</th>
<th width="35px" class="header">Market</th>
<th width="35px" class="header">Job Status</th>
<th width="65px" class="header">Technology</th>
<th width="95px;" class="header">MEI</th>
<th width="95px" class="header">TEO Writer</th>
<th width="75px" class="header">Quote Due</th>
<th width="100px" class="header">Engineer</th>
<th width="75px" class="header">ML Due</th>
<th width="75px" class="header">ML Complete</th>
<th width="75px" class="header">SPEC Due</th>
<th width="75px" class="header">SPEC Complete</th>
<th width="100px" class="header">Install Supervisor</th>
<th width="75px" class="header">MasTec OJD</th>
<th width="75px" class="header">Install Start</th>
<th width="30px" class="header">Install Hours</th>
<th width="75px" class="header">Revised CRCD</th>
<th width="75px" class="header">Latest Ship-To-Site</th>
<th width="30px" class="header">Total Parts</th>
<th width="30px" class="header">OEM Rcvd</th>
<th width="30px" class="header">Minor Rcvd</th>
<th width="30px" class="header">Total Received</th>
<th width="30px" class="header">% On Site</th>
<th width="60px" class="header">Actions</th>
</tr>
Obviously, my table has many more rows than this. 193 to be exact, but you can see where the header row repeats. The repeating header row is set up by this function:
jQuery
// Clone the original header row and add the "repeated-header" class
var tblHeader = $('tr.boxheadrow').clone().addClass('repeated-header');
// Add the cloned header with the new class every 34th row (or as you see fit)
$('tbody tr:odd:nth-of-type(17n)').after(tblHeader);
// On the 'sortStart' routine, remove all the inserted header rows
$('#pmtable').bind('sortStart', function() {
$('.repeated-header').remove();
// On the 'sortEnd' routine, add back all the header row lines.
}).bind('sortEnd', function() {
$('tbody tr:odd:nth-of-type(17n)').after(tblHeader);
});
A lot of people seem to be looking for this answer. I found it buried in an answer to another question here: Syncing column width of between tables in two different frames, etc
Of the dozens of methods I have tried this is the only method I found that works reliably to allow you to have a scrolling bottom table with the header table having the same widths.
Here is how I did it, first I improved upon the jsfiddle above to create this function, which works on both td and th (in case that trips up others who use th for styling of their header rows).
var setHeaderTableWidth= function (headertableid,basetableid) {
$("#"+headertableid).width($("#"+basetableid).width());
$("#"+headertableid+" tr th").each(function (i) {
$(this).width($($("#"+basetableid+" tr:first td")[i]).width());
});
$("#" + headertableid + " tr td").each(function (i) {
$(this).width($($("#" + basetableid + " tr:first td")[i]).width());
});
}
Next, you need to create two tables, NOTE the header table should have an extra TD to leave room in the top table for the scrollbar, like this:
<table id="headertable1" class="input-cells table-striped">
<thead>
<tr style="background-color:darkgray;color:white;"><th>header1</th><th>header2</th><th>header3</th><th>header4</th><th>header5</th><th>header6</th><th></th></tr>
</thead>
</table>
<div id="resizeToBottom" style="overflow-y:scroll;overflow-x:hidden;">
<table id="basetable1" class="input-cells table-striped">
<tbody >
<tr>
<td>testdata</td>
<td>2</td>
<td>3</td>
<td>4</span></td>
<td>55555555555555</td>
<td>test</td></tr>
</tbody>
</table>
</div>
Then do something like:
setHeaderTableWidth('headertable1', 'basetable1');
$(window).resize(function () {
setHeaderTableWidth('headertable1', 'basetable1');
});
This is the only solution that I found on Stack Overflow that works out of many similar questions that have been posted, that works in all my cases.
For example, I tried the jQuery stickytables plugin which does not work with durandal, and the Google Code project here https://code.google.com/p/js-scroll-table-header/issues/detail?id=2
Other solutions involving cloning the tables, have poor performance, or suck and don't work in all cases.
There is no need for these overly complex solutions. Just make two tables like the examples below and call setHeaderTableWidth function like described here and boom, you are done.
If this does not work for you, you probably were playing with your CSS box-sizing property and you need to set it correctly. It is easy to screw up your CSS content by accident. There are many things that can go wrong, so just be aware/careful of that. This approach works for me.
Here's a solution that we ended up working with (in order to deal with some edge cases and older versions of Internet Explorer, we eventually also faded out the title bar on scroll then faded it back in when scrolling ends, but in Firefox and WebKit browsers this solution just works. It assumes border-collapse: collapse.
The key to this solution is that once you apply border-collapse, CSS transforms work on the header, so it's just a matter of intercepting scroll events and setting the transform correctly. You don't need to duplicate anything. Short of this behavior being implemented properly in the browser, it's hard to imagine a more light-weight solution.
JSFiddle: http://jsfiddle.net/podperson/tH9VU/2/
It's implemented as a simple jQuery plugin. You simply make your thead's sticky with a call like $('thead').sticky(), and they'll hang around. It works for multiple tables on a page and head sections halfway down big tables.
$.fn.sticky = function(){
$(this).each( function(){
var thead = $(this),
tbody = thead.next('tbody');
updateHeaderPosition();
function updateHeaderPosition(){
if(
thead.offset().top < $(document).scrollTop()
&& tbody.offset().top + tbody.height() > $(document).scrollTop()
){
var tr = tbody.find('tr').last(),
y = tr.offset().top - thead.height() < $(document).scrollTop()
? tr.offset().top - thead.height() - thead.offset().top
: $(document).scrollTop() - thead.offset().top;
thead.find('th').css({
'z-index': 100,
'transform': 'translateY(' + y + 'px)',
'-webkit-transform': 'translateY(' + y + 'px)'
});
} else {
thead.find('th').css({
'transform': 'none',
'-webkit-transform': 'none'
});
}
}
// See http://www.quirksmode.org/dom/events/scroll.html
$(window).on('scroll', updateHeaderPosition);
});
}
$('thead').sticky();
Here is an improved answer to the one posted by Maximilian Hils.
This one works in Internet Explorer 11 with no flickering whatsoever:
var headerCells = tableWrap.querySelectorAll("thead td");
for (var i = 0; i < headerCells.length; i++) {
var headerCell = headerCells[i];
headerCell.style.backgroundColor = "silver";
}
var lastSTop = tableWrap.scrollTop;
tableWrap.addEventListener("scroll", function () {
var stop = this.scrollTop;
if (stop < lastSTop) {
// Resetting the transform for the scrolling up to hide the headers
for (var i = 0; i < headerCells.length; i++) {
headerCells[i].style.transitionDelay = "0s";
headerCells[i].style.transform = "";
}
}
lastSTop = stop;
var translate = "translate(0," + stop + "px)";
for (var i = 0; i < headerCells.length; i++) {
headerCells[i].style.transitionDelay = "0.25s";
headerCells[i].style.transform = translate;
}
});
I developed a simple light-weight jQuery plug-in for converting a well formatted HTML table to a scrollable table with fixed table header and columns.
The plugin works well to match pixel-to-pixel positioning the fixed section with the scrollable section. Additionally, you could also freeze the number of columns that will be always in view when scrolling horizontally.
Demo & Documentation: http://meetselva.github.io/fixed-table-rows-cols/
GitHub repository: https://github.com/meetselva/fixed-table-rows-cols
Below is the usage for a simple table with a fixed header,
$(<table selector>).fxdHdrCol({
width: "100%",
height: 200,
colModal: [{width: 30, align: 'center'},
{width: 70, align: 'center'},
{width: 200, align: 'left'},
{width: 100, align: 'center'},
{width: 70, align: 'center'},
{width: 250, align: 'center'}
]
});
<html>
<head>
<script src="//cdn.jsdelivr.net/npm/jquery#3.2.1/dist/jquery.min.js"></script>
<script>
function stickyTableHead (tableID) {
var $tmain = $(tableID);
var $tScroll = $tmain.children("thead")
.clone()
.wrapAll('<table id="tScroll" />')
.parent()
.addClass($(tableID).attr("class"))
.css("position", "fixed")
.css("top", "0")
.css("display", "none")
.prependTo("#tMain");
var pos = $tmain.offset().top + $tmain.find(">thead").height();
$(document).scroll(function () {
var dataScroll = $tScroll.data("scroll");
dataScroll = dataScroll || false;
if ($(this).scrollTop() >= pos) {
if (!dataScroll) {
$tScroll
.data("scroll", true)
.show()
.find("th").each(function () {
$(this).width($tmain.find(">thead>tr>th").eq($(this).index()).width());
});
}
} else {
if (dataScroll) {
$tScroll
.data("scroll", false)
.hide()
;
}
}
});
}
$(document).ready(function () {
stickyTableHead('#tMain');
});
</script>
</head>
<body>
gfgfdgsfgfdgfds<br/>
gfgfdgsfgfdgfds<br/>
gfgfdgsfgfdgfds<br/>
gfgfdgsfgfdgfds<br/>
gfgfdgsfgfdgfds<br/>
gfgfdgsfgfdgfds<br/>
<table id="tMain" >
<thead>
<tr>
<th>1</th> <th>2</th><th>3</th> <th>4</th><th>5</th> <th>6</th><th>7</th> <th>8</th>
</tr>
</thead>
<tbody>
<tr><td>11111111111111111111111111111111111111111111111111111111</td><td>2</td><td>3</td><td>4</td><td>5555555</td><td>66666666666</td><td>77777777777</td><td>8888888888888888</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5555555</td><td>66666666666</td><td>77777777777</td><td>8888888888888888</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5555555</td><td>66666666666</td><td>77777777777</td><td>8888888888888888</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5555555</td><td>66666666666</td><td>77777777777</td><td>8888888888888888</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5555555</td><td>66666666666</td><td>77777777777</td><td>8888888888888888</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5555555</td><td>66666666666</td><td>77777777777</td><td>8888888888888888</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5555555</td><td>66666666666</td><td>77777777777</td><td>8888888888888888</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5555555</td><td>66666666666</td><td>77777777777</td><td>8888888888888888</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5555555</td><td>66666666666</td><td>77777777777</td><td>8888888888888888</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5555555</td><td>66666666666</td><td>77777777777</td><td>8888888888888888</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5555555</td><td>66666666666</td><td>77777777777</td><td>8888888888888888</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5555555</td><td>66666666666</td><td>77777777777</td><td>8888888888888888</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5555555</td><td>66666666666</td><td>77777777777</td><td>8888888888888888</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5555555</td><td>66666666666</td><td>77777777777</td><td>8888888888888888</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5555555</td><td>66666666666</td><td>77777777777</td><td>8888888888888888</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5555555</td><td>66666666666</td><td>77777777777</td><td>8888888888888888</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5555555</td><td>66666666666</td><td>77777777777</td><td>8888888888888888</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5555555</td><td>66666666666</td><td>77777777777</td><td>8888888888888888</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5555555</td><td>66666666666</td><td>77777777777</td><td>8888888888888888</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5555555</td><td>66666666666</td><td>77777777777</td><td>8888888888888888</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5555555</td><td>66666666666</td><td>77777777777</td><td>8888888888888888</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5555555</td><td>66666666666</td><td>77777777777</td><td>8888888888888888</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5555555</td><td>66666666666</td><td>77777777777</td><td>8888888888888888</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5555555</td><td>66666666666</td><td>77777777777</td><td>8888888888888888</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5555555</td><td>66666666666</td><td>77777777777</td><td>8888888888888888</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5555555</td><td>66666666666</td><td>77777777777</td><td>8888888888888888</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5555555</td><td>66666666666</td><td>77777777777</td><td>8888888888888888</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5555555</td><td>66666666666</td><td>77777777777</td><td>8888888888888888</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5555555</td><td>66666666666</td><td>77777777777</td><td>8888888888888888</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5555555</td><td>66666666666</td><td>77777777777</td><td>8888888888888888</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5555555</td><td>66666666666</td><td>77777777777</td><td>8888888888888888</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5555555</td><td>66666666666</td><td>77777777777</td><td>8888888888888888</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5555555</td><td>66666666666</td><td>77777777777</td><td>8888888888888888</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5555555</td><td>66666666666</td><td>77777777777</td><td>8888888888888888</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5555555</td><td>66666666666</td><td>77777777777</td><td>8888888888888888</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5555555</td><td>66666666666</td><td>77777777777</td><td>8888888888888888</td></tr>
</tbody>
</table>
</body>
</html>
Additional to #Daniel Waltrip answer. Table need to enclose with div position: relative in order to work with position:sticky . So I would like to post my sample code here.
CSS
/* Set table width/height as you want.*/
div.freeze-header {
position: relative;
max-height: 150px;
max-width: 400px;
overflow:auto;
}
/* Use position:sticky to freeze header on top*/
div.freeze-header > table > thead > tr > th {
position: sticky;
top: 0;
background-color:yellow;
}
/* below is just table style decoration.*/
div.freeze-header > table {
border-collapse: collapse;
}
div.freeze-header > table td {
border: 1px solid black;
}
HTML
<html>
<body>
<div>
other contents ...
</div>
<div>
other contents ...
</div>
<div>
other contents ...
</div>
<div class="freeze-header">
<table>
<thead>
<tr>
<th> header 1 </th>
<th> header 2 </th>
<th> header 3 </th>
<th> header 4 </th>
<th> header 5 </th>
<th> header 6 </th>
<th> header 7 </th>
<th> header 8 </th>
<th> header 9 </th>
<th> header 10 </th>
<th> header 11 </th>
<th> header 12 </th>
<th> header 13 </th>
<th> header 14 </th>
<th> header 15 </th>
</tr>
</thead>
<tbody>
<tr>
<td> data 1 </td>
<td> data 2 </td>
<td> data 3 </td>
<td> data 4 </td>
<td> data 5 </td>
<td> data 6 </td>
<td> data 7 </td>
<td> data 8 </td>
<td> data 9 </td>
<td> data 10 </td>
<td> data 11 </td>
<td> data 12 </td>
<td> data 13 </td>
<td> data 14 </td>
<td> data 15 </td>
</tr>
<tr>
<td> data 1 </td>
<td> data 2 </td>
<td> data 3 </td>
<td> data 4 </td>
<td> data 5 </td>
<td> data 6 </td>
<td> data 7 </td>
<td> data 8 </td>
<td> data 9 </td>
<td> data 10 </td>
<td> data 11 </td>
<td> data 12 </td>
<td> data 13 </td>
<td> data 14 </td>
<td> data 15 </td>
</tr>
<tr>
<td> data 1 </td>
<td> data 2 </td>
<td> data 3 </td>
<td> data 4 </td>
<td> data 5 </td>
<td> data 6 </td>
<td> data 7 </td>
<td> data 8 </td>
<td> data 9 </td>
<td> data 10 </td>
<td> data 11 </td>
<td> data 12 </td>
<td> data 13 </td>
<td> data 14 </td>
<td> data 15 </td>
</tr>
<tr>
<td> data 1 </td>
<td> data 2 </td>
<td> data 3 </td>
<td> data 4 </td>
<td> data 5 </td>
<td> data 6 </td>
<td> data 7 </td>
<td> data 8 </td>
<td> data 9 </td>
<td> data 10 </td>
<td> data 11 </td>
<td> data 12 </td>
<td> data 13 </td>
<td> data 14 </td>
<td> data 15 </td>
</tr>
<tr>
<td> data 1 </td>
<td> data 2 </td>
<td> data 3 </td>
<td> data 4 </td>
<td> data 5 </td>
<td> data 6 </td>
<td> data 7 </td>
<td> data 8 </td>
<td> data 9 </td>
<td> data 10 </td>
<td> data 11 </td>
<td> data 12 </td>
<td> data 13 </td>
<td> data 14 </td>
<td> data 15 </td>
</tr>
<tr>
<td> data 1 </td>
<td> data 2 </td>
<td> data 3 </td>
<td> data 4 </td>
<td> data 5 </td>
<td> data 6 </td>
<td> data 7 </td>
<td> data 8 </td>
<td> data 9 </td>
<td> data 10 </td>
<td> data 11 </td>
<td> data 12 </td>
<td> data 13 </td>
<td> data 14 </td>
<td> data 15 </td>
</tr>
<tr>
<td> data 1 </td>
<td> data 2 </td>
<td> data 3 </td>
<td> data 4 </td>
<td> data 5 </td>
<td> data 6 </td>
<td> data 7 </td>
<td> data 8 </td>
<td> data 9 </td>
<td> data 10 </td>
<td> data 11 </td>
<td> data 12 </td>
<td> data 13 </td>
<td> data 14 </td>
<td> data 15 </td>
</tr>
<tr>
<td> data 1 </td>
<td> data 2 </td>
<td> data 3 </td>
<td> data 4 </td>
<td> data 5 </td>
<td> data 6 </td>
<td> data 7 </td>
<td> data 8 </td>
<td> data 9 </td>
<td> data 10 </td>
<td> data 11 </td>
<td> data 12 </td>
<td> data 13 </td>
<td> data 14 </td>
<td> data 15 </td>
</tr>
</tbody>
</table>
</div>
</body>
</html>
Demo

Highlight rows across two adjacent tables with CSS

I have a layout with two adjacent tables. When a row in one table is hovered, I'd like to apply the same style to the cells in the rows across both tables.
Is this possible with css only?
table{
display: inline-block;
}
td{
border:1px solid grey;
}
table#lhs tr:hover{
background-color:green;
}
table#lhs tr:hover {
background-color:green;
}
<table id="lhs">
<tr>
<td>lhs row 1</td>
</tr>
<tr>
<td>lhs row 2</td>
</tr>
<tr>
<td>lhs row 3</td>
</tr>
</table>
<table id="rhs">
<tr>
<td>rhs row 1</td>
</tr>
<tr>
<td>rhs row 2</td>
</tr>
<tr>
<td>rhs row 3</td>
</tr>
</table>
I came up with this, which seems pretty close to what you were asking for:
table {
display: inline-block;
}
div {
overflow:hidden;
float:left;
}
td {
border:1px solid grey;
}
table#lhs tr:hover {
background-color:green;
}
table#lhs tr:hover {
background-color:green;
}
#lhs td:hover::before {
background-color: green;
content:'\00a0';
position: absolute;
width: 10000px;
z-index: -1;
}
td {
position: relative;
}
<div>
<table id="lhs">
<tr>
<td>lhs row 1</td>
</tr>
<tr>
<td>lhs row 2</td>
</tr>
<tr>
<td>lhs row 3</td>
</tr>
</table>
<table id="rhs">
<tr>
<td>rhs row 1</td>
</tr>
<tr>
<td>rhs row 2</td>
</tr>
<tr>
<td>rhs row 3</td>
</tr>
</table>
</div>