Deep dive: <tables> with sticky headers and columns

Muggle-born
3 min readApr 26, 2024

The bare minimum needed to create a table with both sticky header rows and columns.

One day you’ll be asked to create a table that supports horizontal and vertical scrolling, in which case you’ll likely want to keep the 1st row and column visible to users while scrolling. This guide is for you when that day comes.

Part 1. A Basic Table

<table>
<caption>A Basic Table</caption>

<thead>
<tr>
<th>Header</th>
</tr>
</thead>

<tbody>
<tr>
<td>Text</td>
</tr>
</tbody>
</table>

All tables broken down consist of the same html tags. We can add styles on top of this example which will apply to any table you build

Part 2. CSS Reset

/*
1. Remove text indentation from table contents in Chrome and Safari.
https://bugs.chromium.org/p/chromium/issues/detail?id=999088
https://bugs.webkit.org/show_bug.cgi?id=201297
2. Correct table border color inheritance in all Chrome and Safari.
https://bugs.chromium.org/p/chromium/issues/detail?id=935729
https://bugs.webkit.org/show_bug.cgi?id=195016
3. Remove gaps between table borders by default.
*/
table {
text-indent: 0; /* 1 */
border-color: inherit; /* 2 */
border-collapse: collapse; /* 3 */
}

Use the CSS above to normalize the style of your <table>. If you’re looking for a up-to-date CSS Reset, I’m a huge fan of Tailwind’s preflight.css which is pretty similar to a CSS Reset.

Part 3. Make it scroll

We need two things to make an html element scroll

  1. The parent container needs to specify an overflow CSS property. For example, overflow: auto;
  2. The content needs to not fit in the parent container
<div class="table_wrapper">
<table>...</table>
</div>
.table_wrapper {
overflow: auto; /* (1) turn on overflow */
max-width: 25vw; /* (2) enforce a maximum size that causes overflow */
max-height: 25vh; /* (2) enforce a maximum size that causes overflow */
}

For illustrative purposes, small values were used. However, the max-width and max-height can be whatever you want them to be.

Part 4. Make it sticky

Add some more data to the table and then a few CSS properties and you’ve got yourself a table where the 1st row and column remain visible to users while scrolling.

<div class="table_wrapper">
<table>
<caption>A Basic Table</caption>

<thead>
<tr>
<th>Name</th>
<th>Job Description</th>
</tr>
</thead>

<tbody class="tbody">
<tr>
<th scope="row">Chef</th>
<td>Cook stuff</td>
</tr>
</tbody>
</table>
</div>
.table_wrapper {
position: relative;
...
}

thead th {
position: sticky;
top: 0;
}

thead th:nth-of-type(1) {
left: 0;
}

tbody th {
position: sticky;
left: 0;
}

For illustrative purposes, selectors thead th and tbody th were used. However, it’d be better to use class names.

Part 5. make it look pretty (ft. a border while scrolling)

Try adding a border to the sticky element and scroll in the table. You’ll notice the border doesn’t scroll with the content and you lose your border.

Use box-shadow instead of border

thead th {
z-index: 10;
background-color: white;
/** Use an "inset" box-shadow instead of border-bottom */
box-shadow: inset 0 -1px 0 black;
...
}

thead th:nth-of-type(1) {
z-index: 11;
...
}

tbody th {
z-index: 10;
background-color: white;
/** Use an "inset" box-shadow instead of border-bottom */
box-shadow: inset -1px 0 0 black;
...
}

I’d recommend using this example as a baseline. The styling is very minimal and can easily be improved with usage of thepadding border and background-color CSS properties.

The End

Here’s what our <table> ends up looking like

The <table> created following this blog. It demonstrates an table overflowing while the header column and row remain visible
The <table> created following this blog

Additional Resources

--

--

Muggle-born

Hi, my name is Jeremiah. I build things on the web for fun. I enjoy figuring how and why things work the way they do, and I try teaching others along the way.