We’ve covered that individual
<td> can be
position: sticky. It’s pretty easy to make the header of a table stick to the top of the screen while scrolling through a bunch or rows (like this demo).
But stickiness isn’t just for the top of the screen, you can stick things in any scroll direction (horizontal is just as fun). In fact, we can have multiple sticky elements stuck in different directions inside the same element, and even single elements that are stuck in multiple directions.
Here’s a video example of a table that sticks both the header and first column:
Why would you do that? Specifically for tabular data where cross-referencing is the point. In this table (which represents, of course, the scoring baseball game where somehow 20 teams are all playing each other at once because that’s how baseball works), it “makes sense” that you wouldn’t want the team name or the inning number to scroll away, as you’d lose context of what you’re looking at.
Not all tables need to be bi-directionally cross-referenceable. A lot of tables can smash rows into blocks on small screens for a better small-screen experience.
The “trick” at play here is partially the
position: sticky; usage, but moreso to me, how you have to handle overlapping elements. A table cell that is sticky needs to have a background, because otherwise we’ll see overlapping content. It also needs proper
z-index handling so that when it sticks in place, it’ll be on top of what it is supposed to be on top of. This feels like the trickiest part:
- Make sure the
tbody>thcells are above regular table cells, so they stay on top during a horizontal scroll.
- Make sure the
thead>thcells are above those, for vertical scrolling.
- Make sure the
thead>th:first-childcell is the very highest, as it needs to be above the body cells and it’s sibling headers again for horizontal scrolling.
A bit of a dance, but it’s doable.
High five to Cameron Clark who emailed me demoed this and showed me how cool it is. And indeed, Cameron, it is cool. When I shared that around, Estelle Weyl showed me a demo she made several years ago. That feels about right, Estelle is always a couple of years ahead of me.
I have them both beat ;)
These are cool solutions :-)
Here is my take on this:
Sources are located at:
I had a similar need some time ago, when I needed to build a product comparison table, and I realized that having the first column always visible can be problematic on small screens, where there isn’t enough space. Instead, I preferred adding the label as custom attribute and fetch it with CSS attr(). I did use horizontal stickiness too, though, for the intermediate sections.
Here’s the respective pen and a more detailed explanation.
Any solutions like this that include virtualization for massive tables?
All browsers will support th/thead sticky by the end of the year.
“you can’t position tr or thead” is not true in Firefox/Safari, and is a crbug.com/702927 in Chrome. Chrome’s table rewrite is almost done, and with it, sticky bug will be fixed.
Would it be possible to post the HTML as well as the CSS please?
Try clicking the “View Compiled” button on the CodePen demo. That will compile Pug into HTML for ya.
Have you tried this out using any assistive tech (VoiceOver, JAWS, etc)? Historically, they don’t have a great time whenever you get CSS to do anything “elaborate” on a table and get confused.
It looks a great attempt otherwise however.
Wayne, this works in JAWS, VO, and NVDA because the wrapper has an accessible name & appropriate role, and the table has no display properties overriding its semantics.
TalkBack on Android is not so good. I filed a bug in December because this technique can cause trouble: TalkBack does not recognize tables in parents with tabindex.
I have screen reader support notes in my post Under-Engineered Responsive Tables, explaining the attributes.
For positioning in Safari you may need to use
<caption>may misbehave as well (be sure to test). I go into more detail and outline workarounds in Fixed Table Headers.
You could set also 2nd column sticky, just need to know its position from the left. In other words, you need to know the size of the 1st column.
I used custom CSS properties for “fixed” columns sizes. Then used JS to make “fixed” columns resizable by user.
Works like a charm! :)
Can you provide the css to do this?
Clever solution! My only problem is if I want cell borders, I have basically two options: 1) use border-collapse: collapse or 2) use border-collapse: separate. In the first case, I can have 1px cell borders, but they will disappear when I scroll down the table. In the second case, the cell borders are always there, but they are at least 2px wide. Is there a way to solve this to have always visible, 1px cell borders around header cells?
I could solve my problem using border-collapse: collapse and a bit of border trickery: https://jsfiddle.net/koldev/1kwg3arq/
I have this table and unable to fix the header, could someone please help me to fix this issue.
Got the issue -> the sticky not working – tbody – thead with below css .
any workaround ?
I have been wasting a couple of hours trying to do the same with position:absolute for 2 fixed columns on the left and one fixed column on the right running into all kinds of troubles (cells not having the same height anymore), etc. And all it needs is to be sticky (+ some positioning) … THANKS!
The problem with all of these solutions is that the horizontal scrollbar is not correct. It spans the full width of the table, rather than the actual scrollable area. Still searching for a solution to this
Did you find any solution for horizontal scrollbar?
I am also in the same boat as you. Did you find any solution?
Is there a way to keep the top sticky, but also the left 2 columns (instead of just 1 column)?
Let me tell you that you, sir, have saved my life. Thank you for this
Is it possible to make headers use “resize: horizontal” with the first column sticky?
Great implementation. Thanks.
tableselector is styled as
border: 1px solid black;. I think the second one is unnecessary and it overrides the first one. Beside, it causes a small flickering, when you scroll the table horizontally. Because the
1pxborder but the