CSS – Aligned scrollable table body with fixed header

The problem

I wanted to include a component that rendered a list of entries that was scrollable whilst having a fixed header. I felt that this should have been straightforward, but it brought up a few issues along the way.

Here’s where I ended up:

Simple scrollable list

I’m using a simple html table format for this as follows:

render() {
return (
<div className="placeholder" id="monthlyList">
<table className="table">
<thead>
<tr>
<th scope="col" className="col-3">Date</th>
<th scope="col" className="col-3">Type</th>
<th scope="col" className="col-3">Retailer</th>
<th scope="col" className="col-3">Value</th>
</tr>
</thead>
<tbody>
{this.renderMonthlyList(this.props.monthData)}
</tbody>
</table>
</div>
);
}
view raw App.js hosted with ❤ by GitHub
Template JSX table

…and the renderMonthList function has the following structure:

renderMonthlyList = (monthlyEntryList) => {
let options = [];
if (monthlyEntryList && monthlyEntryList.length !== 0) {
monthlyEntryList.forEach((entry, index) => {
options.push(<tr key={`listRow${index}`}>
<td className="col-3" >{entry.date}</td>
<td className="col-3" >{entry.type}</td>
<td className="col-3" >{entry.retailer}</td>
<td className="col-3" >{formatCurrency(entry.currency, entry.value)}</td>
</tr>
)
})
}
return options
};
view raw App.js hosted with ❤ by GitHub
Render list data

Just an aside before the main issue – I like this way of rendering a list of items. First checking that we have some data to show in the if statement, then pushing the required JSX elements into an array which is then rendered when the ‘renderMonthlyList’ function is called upon. Anyways, back to the problem (scroll straight to the bottom if you just want the answer!):

Issues:
scrolling header then no scrolling,
alignment issues

Initially I set CSS properties on the component level rather than on the JSX table itself. I added a height attribute as well as “overflow: auto”. The height defines the size of the visible area while the overflow: auto lets the list be scrollable. However, this made the whole table scrollable, meaning that the header went out of shot too.

Realising that I wanted the header to be fixed, I set the height and overflow attributes to the tbody tag, but then I lost all scrolling and the list ran outside of its boundaries.

The following stack overflow query noted that I needed to make tbody and thead as block elements. This gave me the scrolling effect, but when I did this then sometimes the headers weren’t inline with the data, and other times (adjusting some other attributes) then all the data fell under the first header. Very annoying.

This stack overflow query said that we should use ‘table-header-group’ as a display attribute instead. But this meant that I lost scrolling again.

Solution

This person gave me the solution I needed.

Tbody needed to be set as block (as noted in the previous post) and given a ‘table-layout: fixed’ setting.
Thead and the rows of tbody needed to be set to ‘display: table’, also with a ‘table-layout: fixed’ characteristic.

While I’m not sure how this exactly works, my guess is that:

  • tbody = block makes the body scrollable (as noted in the stack overflow answer)
  • having thead and the rows of tbody as ‘display: table’ links their behaviour so that they’re aware of each other and act as a table (this could be completely wrong!)
  • setting the ‘table-layout: fixed’ to both body and header sets their alignment.

This gave me exactly what I needed. A scrollable list with a fixed header where all the items aligned with each other.

This is the final CSS that got me there, along with the HTML as noted above:

table {
width: 100%;
}
thead, tbody, tr {
display: table;
width: 100%;
table-layout: fixed;
}
tbody {
display: block;
overflow: auto;
table-layout: fixed;
max-height: 250px;
}
view raw App.css hosted with ❤ by GitHub
CSS for scrollable table body with aligned header

And that’s it. I hope that helps someone out there who’s had the same issues I’ve had. If this helped you, I’d love to hear from you!