<code>position: sticky;</code> is far from bug free, but it helps for very basic tables.<br>
<br>
<br>
Slowly scroll down to see the table headers move.<br>
(and for the last table scroll left/right)<br>
<hr class="margin_v_xl">
Table with both sticky top and footer headers:<br>
<table border=1>
<thead>
<tr>
<th>th1</th>
<th>th2</th>
<th>th3</th>
</tr>
</thead>
<tbody>
<tr><td>1</td><td>2</td><td>3</td></tr>
<tr><td>1</td><td>2</td><td>3</td></tr>
<tr><td>1</td><td>2</td><td>3</td></tr>
</tbody>
<thead>
<tr>
<th>th1</th>
<th>th2</th>
<th>th3</th>
</tr>
</thead>
</table>
<hr>
Table with both sticky top and footer (thead and tfoot):<br>
<table border=1>
<thead>
<tr>
<th>th1</th>
<th>th2</th>
<th>th3</th>
</tr>
</thead>
<tbody>
<tr><td>1</td><td>2</td><td>3</td></tr>
<tr><td>1</td><td>2</td><td>3</td></tr>
<tr><td>1</td><td>2</td><td>3</td></tr>
</tbody>
<tfoot>
<tr>
<th>th1</th>
<th>th2</th>
<th>th3</th>
</tr>
</tfoot>
</table>
<hr>
Table with 3 <tr> rows of header cells, with some rowspan and colspan in the mix:<br>
<table border=1>
<thead>
<tr>
<th>r1 th1</th>
<th rowspan="2">r1 th2 rowspan=2</th>
<th colspan="2">r1 th3 colspan=2</th>
</tr>
<tr>
<th>r2 th1</th>
<th>r2 th2</th>
<th rowspan="2">r2 th2</th>
</tr>
<tr>
<th>r3 th1</th>
<th>r3 th2</th>
<th>r3 th2</th>
</tr>
</thead>
<tbody>
<tr><td>1</td><td>2</td><td>3</td><td>4</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td></tr>
</tbody>
</table>
<hr>
Table with sticky header columns on the left (1,2) and right (5,6) side:<br>
<table border=1...
CSS
table thead tr th,
table tfoot tr th {
position: sticky;
position: --webkit-sticky;
top: 0;
}
/* Assume thet final child in <table>
is the footer <thead> and align to bottom (if exists):
*/
table thead:last-child tr th,
table tfoot tr th {
bottom: 0;
}
/* if a <thead> has 2 rows, put the 2nd row a bit more down:
Note that the 1.5em is a hardcoded value that seems to about match for default borders,
but it does not take into account the real size of the 1st row in any way.
Also you need to repeat this for each additional <tr>.
*/
table thead tr:nth-child(2) th,
table tfoot tr:nth-child(2) th {
top: 1.5em;
}
table thead tr:nth-child(3) th,
table tfoot tr:nth-child(3) th {
top: 3em;
}
table tbody tr th {
position: sticky;
position: --webkit-sticky;
}
table tbody tr th:first-child {
left: 0;
}
/* Note it is fairly impossible to guess a good hardcoded left value,
especially if first column width is unknown or dynamic.
*/
table tbody tr th:nth-child(2) {
left: 200px;
}
table tbody tr th:last-child {
right: 0;
}
/* Note it is fairly impossible to guess a good hardcoded right value,
especially if last column width is unknown or dynamic.
*/
table tbody tr th:nth-last-child(2) {
right: 200px;
}
/* make table cells a big bigger,
to more easily create larger tables with just little html code: */
table tbody th ,
table tbody td {
width: 100px;
height: 100px;
}
table th {
background-color: rgba(128, 128, 128, 0.5);
}
/* large vertical margin so we have plenty of room to scroll up/down and see the headers move:
*/
.margin_v_xl {
margin: 200px 0;
}
.margin_top_xl {
margin-top: 200px;
}
.margin_bottom_xl {
margin-top: 200px;
}
Please Whitelist JSFiddle in your content blocker.
Help keep JSFiddle free for always by one of two ways:
Whitelist JSFiddle in your content blocker (two clicks)
Go PRO and get access to additional PRO features →
Join the 4+ million users, and keep the JSFiddle dream alive.
Ad-free
All ads in the editor and listing pages are turned completely off.
Use pre-released features
You get to try and use features (like the Palette Color Generator) months before everyone else.
Fiddle collections
Sort and categorize your Fiddles into multiple collections.
Private collections and fiddles
You can make as many Private Fiddles, and Private Collections as you wish!
Console
Debug your Fiddle with a minimal built-in JavaScript console.