I've not found pure html/css solution that does the following:
uses tables semantically
fixes both header and column
works with variable width columns
works with vertical and horizontal scrolling
Here's something I hacked together that does work
The thing I don't like about it is that the content in the header th elements is duplicated. I think with a little additional hacking this could be fixed.
In the spirit of putting code in the answer, here's the code:
html:
css:
td, th {
padding: 5px;
white-space: nowrap;
}
td {
background: linear-gradient(135deg, white 0%, #a80077 99%, black 100%);
}
th {
height: 0;
font-weight: normal;
}
.scrollx {
max-width: 100%;
overflow-x: scroll;
}
.scrolly {
position: relative;
max-height: 150px;
overflow-y: scroll;
margin-bottom: 20px;
}
table {
border-collapse: collapse;
min-width: 100%;
}
table td.fill,
table th.fill {
width: 100%;
min-width: 0;
background: white;
padding: 0;
}
tr {
position: relative;
}
.sticky {
text-decoration: underline;
font-weight: 700;
}
.stuck {
position: absolute;
}
thead th {
position: relative;
}
thead th div {
padding: 5px;
background: linear-gradient(135deg, black 0%, #a80077 99%, white 100%);
color: #ffffff;
position: absolute;
z-index: 2;
top: 0;
left: 0;
right: 0;
}
js:
$(function() {
$("#status").text('loaded');
$("table").each(function(index, table) {
var firstRow = $($(table).find('tr')[0]);
var offset = 0;
var stickies = firstRow.find('.sticky');
firstRow.children().each(function(index, td) {
var width = $(td).width();
$(table).find('tr td:nth-of-type('+(index+1)+')').css({width: width + 'px'});
$(table).find('tr th:nth-of-type('+(index+1)+')').css({width: width + 'px'});
});
stickies.each(function(index, td) {
var column = $(table).find('tr .sticky:nth-of-type('+(index+1)+')');
column.css({left: offset+'px'});
column.addClass('stuck');
offset += $(td).width() + 10;
});
$(table).parent().css({"margin-left": offset+'px'});
$(table).parent().parent().scroll(function(e) {
var top = e.currentTarget.scrollTop;
$(table).find('thead tr th div').css({top:top+'px'});
});
});
});
So, what's this doing?
it's using absolute positioning on the fixed (or "sticky") columns to keep the stuck on the left. To support variable widths, it's calculating the width before setting the position to absolute. Then a margin is applied to the left of the table container to compensate for the absolutely positioned columns.
For fixing the header, it's absolutely positioning the nested divs on the th elements, and then adjusting their "top" css property whenever the table is scrolled. Why the duplicated content? It's there so that the column width correctly takes into account the width of the header content.
This is a pretty rough implementation - the jquery code here is meant to be a proof of concept.