<div id="container"></div>
thead th {
padding: 0;
background: red;
}
.end-buffer-area {
z-index: -1;
position: relative;
}
.header-column {
height: auto !important;
padding: 10px;
box-sizing: border-box;
}
.stickyHeader .header-column {
position: fixed;
top: 0;
background: inherit;
animation: slideDown 200ms ease-in;
}
@keyframes slideDown {
0% {
transform: translateY(-50%);
}
100% {
transform: translateY(0%);
}
}
function StickyHeaderWrapper({
children,
}) {
const el = React.useRef(null);
const startEl = React.useRef(null);
const endEl = React.useRef(null);
React.useEffect(() => {
if (el.current) {
const show = () => {
if (!el.current) {
return;
}
(el.current.querySelectorAll('.header-column') || []).forEach(
col => {
if (!col.parentElement) { return; }
const { width, height } =
col.parentElement.getBoundingClientRect() || {};
col.style.width = col.parentElement.style.width = `${width}px`;
col.style.height = col.parentElement.style.height = `${height}px`;
`${width}px`;
}
);
el.current.classList.add("stickyHeader");
};
const hide = () => {
if (!el.current) {
return;
}
el.current.classList.remove("stickyHeader");
};
if (startEl.current && endEl.current) {
const thead = el.current.querySelectorAll('thead');
const rows = el.current.querySelectorAll('tr');
const theadHeight = (thead && thead[0].getBoundingClientRect() || {}).height || 0;
const lastRowHeight = (rows && rows[rows.length - 1].getBoundingClientRect() || {}).height || 0;
endEl.current.style.top = `-${theadHeight + lastRowHeight/2}px`;
const states = new Map();
const observer = new IntersectionObserver(
entries => {
entries.forEach(e => {
states.set(e.target, e.boundingClientRect);
});
const { top } = states.get(startEl.current) || {};
const { top: bottom } = states.get(endEl.current) || {};
if (top < 0 && bottom > 0) {
show();
} else {
hide();
}
},
{
threshold: [0],
}
);
observer.observe(startEl.current);
observer.observe(endEl.current);
}
}
}, []);
return (
<div className="wrapper" ref={el}>
<div ref={startEl} />
{children}
<div className="end-buffer-area" ref={endEl} />
</div>
);
}
const data = [
{
name: 'John Brown',
age: 32,
address: 'New York No. 1 Lake Park',
},
{
name: 'Jim Green',
age: 42,
address: 'London No. 1 Lake Park',
},
{
name: 'Joe Black',
age: 32,
address: 'Sidney No. 1 Lake Park',
},
{
name: 'Jim Red',
age: 32,
address: 'London No. 2 Lake Park',
},
{
name: 'John Brown',
age: 32,
address: 'New York No. 1 Lake Park',
},
{
name: 'Jim Green',
age: 42,
address: 'London No. 1 Lake Park',
},
{
name: 'Joe Black',
age: 32,
address: 'Sidney No. 1 Lake Park',
},
{
name: 'Jim Red',
age: 32,
address: 'London No. 2 Lake Park',
},
{
name: 'John Brown',
age: 32,
address: 'New York No. 1 Lake Park',
},
{
name: 'Jim Green',
age: 42,
address: 'London No. 1 Lake Park',
},
{
name: 'Joe Black',
age: 32,
address: 'Sidney No. 1 Lake Park',
},
{
name: 'Jim Red',
age: 32,
address: 'London No. 2 Lake Park',
},
{
name: 'John Brown',
age: 32,
address: 'New York No. 1 Lake Park',
},
{
name: 'Jim Green',
age: 42,
address: 'London No. 1 Lake Park',
},
{
name: 'Joe Black',
age: 32,
address: 'Sidney No. 1 Lake Park',
},
{
name: 'Jim Red',
age: 32,
address: 'London No. 2 Lake Park',
},
];
function Table() {
return (
<div>
<h1>Sticky header wrapper</h1>
<StickyHeaderWrapper>
<table>
<thead>
<tr>
<th><div className="header-column">Name</div></th>
<th><div className="header-column">Age</div></th>
<th><div className="header-column">Address</div></th>
</tr>
</thead>
<tbody>
{data.map((d, i) => (
<tr key={i}>
<td>{d.name}</td>
<td>{d.age}</td>
<td>{d.address}</td>
</tr>
))}
</tbody>
</table>
</StickyHeaderWrapper>
<div style={{ height: '300px' }}>Blank below</div>
</div>
);
}
ReactDOM.render(<Table />, document.getElementById('container'));
View Compiled
This Pen doesn't use any external CSS resources.