Problem description:
I have a layout where a fixed menu occupies the left side of the viewport, taking up the full height. On the right side, I have a table that dynamically loads data as you scroll. The table includes both a header and a footer.
The issue arises with the table header:
The header stays sticky at the top when scrolling vertically. However, when I set overflow-x: auto
on the .content
container to allow horizontal scrolling, the sticky behavior of the header breaks, and it no longer remains fixed at the top.
What I tried:
I applied position: sticky; top: 0;
to the thead
to keep it fixed. I wrapped the table inside a div with overflow-x: auto
, but this causes the sticky behavior to stop working. I also tried setting position: sticky
directly on the th
element, but that didn’t help. I attempted to use display: block;
on the table header, but it affected the column alignment.
What I expected:
I want the table header to remain sticky at the top of the viewport when scrolling vertically, while also allowing horizontal scrolling within the .content
div.
let rowCount = 20;
const tbody = document.querySelector('#dataTable tbody');
// Function to generate a table row (simulate fetched data)
function generateRow(index) {
return `
<tr>
<td>Row ${index} - Fixed</td>
<td>Row ${index} - Col2</td>
<td>Row ${index} - Col3</td>
<td>Row ${index} - Col4</td>
<td>Row ${index} - Col5</td>
<td>Row ${index} - Col6</td>
<td>Row ${index} - Col7</td>
<td>Row ${index} - Col8</td>
<td>Row ${index} - Col9</td>
<td>Row ${index} - Col10</td>
</tr>
`;
}
// Insert initial rows
for (let i = 1; i <= rowCount; i++) {
tbody.insertAdjacentHTML('beforeend', generateRow(i));
}
// Auto-load more rows when scrolling near the bottom of the page
let loading = false;
window.addEventListener('scroll', function() {
if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 100 && !loading) {
loading = true;
// Simulate API call delay (replace with your actual API call)
setTimeout(() => {
for (let i = rowCount + 1; i <= rowCount + 10; i++) {
tbody.insertAdjacentHTML('beforeend', generateRow(i));
}
rowCount += 10;
loading = false;
}, 300);
}
});
.container {
max-width: 1350px;
padding: 0;
@media (max-width: 1350px) {
padding: 0 15px;
}
@media (max-width: 991.98px) {
padding: 0 15px;
}
}
.d-flex {
display: flex !important;
}
.align-items-start {
align-items: flex-start !important;
}
.gap-30 {
gap: 30px;
}
.menu-main-wrapper {
background: linear-gradient(93.39deg, rgba(113, 113, 113, 0.62) 0.08%, rgba(113, 113, 113, 0) 79.29%);
padding: 0 1px;
height: 100svh;
width: 25.25%;
position: -webkit-sticky;
position: sticky;
z-index: 10;
top: 0;
}
@media screen and (max-width: 1133.98px) {
.menu-main-wrapper {
display: none;
}
}
.page-main-wrapper {
width: 74.75%;
}
@media screen and (max-width: 1133px) {
.page-main-wrapper {
width: 100%;
}
}
.content {
width: 100%;
}
.table-wrapper {
width: 100%;
}
/* Table styling */
table {
border-collapse: separate;
border-spacing: 0;
width: 100%;
min-width: 100px;
margin-bottom: 20px;
}
th,
td {
padding: 10px;
white-space: nowrap;
border-bottom: 1px solid green;
}
thead th {
position: sticky;
top: 0;
background: #686161;
z-index: 2;
}
th:first-child,
td:first-child {
position: sticky;
left: 0;
background: #7e4545;
z-index: 1;
border-right: 1px solid blue;
}
thead th:first-child {
z-index: 3;
border-top-left-radius: 10px;
border-top: 1px solid red;
}
.header,
.footer {
height: 100px;
background: blue;
text-align: center;
margin: 30px 0;
}
<div class="container">
<div class="d-flex align-items-start gap-30">
<div class="menu-main-wrapper">
</div>
<div class="page-main-wrapper">
<div class="header">header</div>
<div class="content">
<div class="table-wrapper">
<table id="dataTable">
<thead>
<tr>
<th>Fixed Column</th>
<th>Column 2</th>
<th>Column 3</th>
<th>Column 4</th>
<th>Column 5</th>
<th>Column 6</th>
<th>Column 7</th>
<th>Column 8</th>
<th>Column 9</th>
<th>Column 10</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
<div class="footer">
</div>
</div>
</div>