You can’t position: sticky; a <thead> . Nor a <tr> . But you can sticky a <th> , which means you can make sticky headers inside a regular ol’ <table> . This is tricky stuff, because if you didn’t know this weird quirk, it would be hard to blame you. It makes way more sense to sticky a parent element like the table header rather than each individual element in a row.

The issue boils down to the fact that stickiness requires position: relative to work and that doesn’t apply to <thead> and <tr> in the CSS 2.1 spec.

There are two very extreme reactions to this, should you need to implement sticky table headers and not be aware of the <th> workaround.

Don’t use table markup at all. Instead, use different elements ( <div> s and whatnot) and other CSS layout methods to replicate the style of a table, but not locked out of using position: relative and creating position: sticky parent elements.

Instead, use different elements ( s and whatnot) and other CSS layout methods to replicate the style of a table, but not locked out of using and creating parent elements. Use table elements, but totally remove all their styling defaults with new display values.

The first is dangerous because you aren’t using semantic and accessible elements for the content to be read and navigated. The second is almost the same. You can go that route, but need to be really careful to re-apply semantic roles.

Anyway, none of that matters if you just stick (get it?!) to using a sticky value on those <th> elements.

See the Pen

Sticky Table Headers with CSS by Chris Coyier (@chriscoyier)

on CodePen.

It’s probably a bit weird to have table headers as a row in the middle of a table, but it’s just illustrating the idea. I was imagining colored header bars separating players on different sports teams or something.

Anytime I think about data tables, I also think about how tricky it can be to make them responsive. Fortunately, there are a variety of ways, all depending on the best way to group and explore the data in them.