Fixed Table Headers
Standard HTML tables are not that complex. But when developers are unfamiliar with HTML or let third-party libraries generate them, they are usually an inaccessible over-engineered mess. One of the more common reasons I hear developers reach for them is because they want fixed headers. For simple tables, that is mostly unnecessary.
Everything in this post assumes a left-to-right, top-to-bottom reading order and language.
13 June 2021: Chrome 91 and Safari 14 got some important updates. They both support making sticky and you no longer need -webkit-sticky for Safari. See the update at the end of this post for screen shots and a new demo you can test. The information about row headers in this post still applies.
The CSS
If you plan to have both in one site (page, screen, whatever), then you may want to avoid conflicts with a more specific selector. After all, row headers will not work properly without scope=»row» , but column headers do just fine without a scope (and in my experience are generally absent).
The different z-index values help ensure your column headers sit in front of your row headers. Otherwise the visible row header will look like it is for the column headers and that is just weird. While you are at it, make sure the header cells have a background color or you will get layered text.
For row headers, you may want to add a border on the right to make the clipping of adjacent cells a bit less weird. The problem is that CSS borders don’t work here. They just scroll away. A little trickery with a background gradient can solve that problem:
Examples
Responsive Uses
Obviously this approach can work well for responsive tables. I have shown how to use a scrolling container. That means we will definitely have a wrapper container and it also means it is likely to scroll independent of the page.
Note my selector is intentionally convoluted. I want to ensure that any overflow styles won’t be applied unless the container has tabindex (for keyboard users), along with a region role and accessible name (for screen reader users). Feel free to adjust for your own uptightness and target browser support.
div[tabindex="0"][aria-labelledby][role="region"]
Scrollbars are often hidden by default on mobile devices (and in some desktop browser configurations), so the visual affordance that the user needs to scroll is often gone. Using Lea Verou’s 2012 post Pure CSS scrolling shadows with background-attachment: local will add a bit of a shadow for the vertically-scrolling content, and Chen Hui Jing adapted it to a horizontal scroll. I tweaked them to use em s instead of px so they will scale better.
div[tabindex="0"][aria-labelledby][role="region"].rowheaders < background: linear-gradient(to right, transparent 30%, rgba(255,255,255,0)), linear-gradient(to right, rgba(255,255,255,0), white 70%) 0 100%, radial-gradient(farthest-side at 0% 50%, rgba(0,0,0,0.2), rgba(0,0,0,0)), radial-gradient(farthest-side at 100% 50%, rgba(0,0,0,0.2), rgba(0,0,0,0)) 0 100%; background-repeat: no-repeat; background-color: #fff; background-size: 4em 100%, 4em 100%, 1.4em 100%, 1.4em 100%; background-position: 0 0, 100%, 0 0, 100%; background-attachment: local, local, scroll, scroll; >div[tabindex="0"][aria-labelledby][role="region"].colheaders < background: linear-gradient(white 30%, rgba(255,255,255,0)), linear-gradient(rgba(255,255,255,0), white 70%) 0 100%, radial-gradient(farthest-side at 50% 0, rgba(0,0,0,.2), rgba(0,0,0,0)), radial-gradient(farthest-side at 50% 100%, rgba(0,0,0,.2), rgba(0,0,0,0)) 0 100%; background-repeat: no-repeat; background-color: #fff; background-size: 100% 4em, 100% 4em, 100% 1.4em, 100% 1.4em; background-attachment: local, local, scroll, scroll;
Larger tables on narrow or short screens can end up scrolling in two directions. A WCAG auditor may argue this violates WCAG 2.1 SC 1.4.10: Reflow (Level AA), but data tables have an exception. Regardless, you should ensure the column header for the row headers does not disappear when scrolling left-right.
The easiest way to do that is grab the first header cell that is not also a row header and increase its z-index , making sure to also give it a border effect as I covered above:
Example
Compatibility Notes
- Only Firefox supports position: sticky on , so for maximum compatibility we have to lean on (and few manual coders use anyway). There is a Chromium bug acknowledging this with some detail; there was a legacy-Edge bug that provided a lot more context but that never made it into the Wayback, so, yeah.
- 3 June 2021: Some confirmation on the Twitters that Chrome 91 got the fix from the bug I cited, which you can confirm in this demo. It seems tables got a full re-write in Chrome.
- 16 June 2021: There is finally a proper confirmation from the Chrome team in the post TablesNG Resolves 72 Chromium Bugs for Better Interoperability .
- 13 June 2021: Safari 14 on macOS and iOS no longer requires -webkit-sticky .
- 13 June 2021: This is still true in Safari 14 on macOS and iOS. However, since that version supports position: sticky on , you can switch to using .
visually-hidden Class
/* Proven method to visually hide something but */ /* still make it available to assistive technology */ .visually-hidden < position: absolute; top: auto; overflow: hidden; clip: rect(1px 1px 1px 1px); /* IE 6/7 */ clip: rect(1px, 1px, 1px, 1px); width: 1px; height: 1px; white-space: nowrap; >
Update: 29 September 2020
Léonie Watson has just posted How screen readers navigate data tables where she walks through a sample table to get some information, explaining each step, the keyboard commands, and the output. She also links to a video demonstration, which I have embedded below.
Update: 23 June 2021
This post does not use CSS scroll snap. That is intentional. Two reasons why I did not implement it:
- A Chrome bug from 2018, Issue 835301: [css-scroll-snap][position-sticky] scroll-snapping “stickily” positioned elements can cause inaccurate snap positions , which was not fixed with the Chrome 91 table fixes;
- In testing, users often wanted to straddle a cell to compare with others, and on smaller screens, higher zooms, or fuller cells, this became particularly difficult.
I forked the pen, added scroll-snap-align: start to the th s and td s, and added scroll-snap-type: both mandatory to the wrapper. You will see in Chrome the caption gets clipped on vertical scroll and the second cell can be clipped on horizontal scroll.
Once you factor in users who resize text or zoom, things can get problematic. Test your scroll snap solution against WCAG SCs 1.4.4 Resize Text, 1.4.10 Reflow, and 1.4.12 Text Spacing. Then try it with just the keyboard.
If you want to use scroll snap, test the heck out of it both with users and in browsers.
Tags
Other Posts
28 Comments
Reply
Larger tables on narrow or short screens can end up scrolling in two directions. It may violate WCAG 2.1 SC 1.4.10: Reflow (Level AA)…
For data tables, they generally fall into the 2-dimensional exception. The example does the right thing in wrapping each so that scrolling is restricted to that table.
That’s why the SC starts with “Content” rather than “Pages”, so that even if you have some content with horizontal scrolling, the page doesn’t need to.
In response to AlastairC. Reply
Alastair, yeah, I should have linked this reflow Understanding statement: Complex data tables have a two-dimensional relationship between the headings and data cells. This relationship is essential to convey the content. This Success Criterion therefore does not apply to data tables.
And I should have qualified it a bit more too to explain why I said may (because of overzealous auditors). Thanks for the comment.
Reply
Thank you so much hat was so simple. You sir, are a master.
In response to Kamlesh Kar. Reply
That is nice of you to say, but mostly I am building on others’ work.
Reply
It was really nice example. It really save my life today,
Thanks manReply
Thank you Adrian. Simple and to the point.
Reply
Thank you very much for giving me insight
Reply
Thank you for your article!
I am looking for a bulletproof solution for sticky table-heading because there is an issue in Safari 14 (macOS) if the position is applied on the .
I tried to apply it on the but Chrome don’t like it.
I will continue to looking for this magic-solution… if you found a way to make it bulletproof, let me know!
In response to Chris. Reply
Chris, the HTML you included was eaten by the commenting system. Only some elements are allowed, and they are for marking up the comment. Can you tell me in plain text what elements you meant to reference in the second and third sentences.
Reply
Tks a lot!! It worked like charm!!
Reply
Joke SON – JOKE – wouldn’t it be nice if this really worked.
Yea, IF the table is at the very TOP of the page it works but not for REAL LIFE situations.In response to Joker Bob. Reply
On the one hand, there is the opening quote where I acknowledge position: sticky is not perfect (which I cover within and at the end of this post).
On the other hand, the second embedded example shows three tables that are not at the top of their page. While I cannot share client projects where I have implemented this (these so-called real life situations), I can assure you that it can work and typically does when you factor in the compatibility notes at the end of this post.
So, shrug emoji?
Reply
In my table, there are two lines of table column header, how can I fix them? Can you give a clue about this? Thanks.
In response to volcano. Reply
Short answer: until more than Firefox supports position: sticky on , no.
Longer answer: barring some special cases, and corresponding mark-up, a column/row header generally should not consist of more than one . If your table fits into one of those scenarios, then the bottom cells will be the ones that stick with this code and that should be fine since the grouping cells would ideally only be referential after the user has started to interact.
Reply
I swear. I’m looking this tutorial on many websites and not found it. Finally i can use it on my website table.
Thank you so much. goodbye javascript, now i just use CSS only for my table. Thanks again.
Reply
does not work on a tablet…
In response to ifo. Reply
Can you provide more detail? What kind of tablet? Android or iOS/iPadOS (or another)? What browser? Which version of browser/OS? Which overflow axis (or both)? With that info I can test it.
The Compatibility Notes above goes into some known issues across browsers and versions, so if it’s not covered there I would like to log it. Any info you can provide would help.
In response to Adrian Roselli. Reply
Thank you. I’m using a google tablet, pixel c to be exact, with brave and chrome that are probably on the latest editions. The code works fine on a laptop. If you would like I can email you the file.
Reply
Hi Adrian,
your solution is clear to the point. There should be no problems, regarding that CanIUse tells us, all modern browsers do interpret this technique.
But I do have the impression, that this is wrong. I encountered the problem with a huge table. To cross-check I took your first example and uploaded it on my webspace – because Codepens iframe seem to be misleading in this case.
You can see your example on my server . All my tests on Android browsers failed. In Chrome, Edge, Firefox and Brave nothing stuck.
Did I miss the news that all browser engines were losing instead of gaining abilities?
I did not delete anything from your code.
Same with the huge table which was the root of my encounters. This table works fine on desktop but not on mobile.
I don’t understand it.
Greetings from Germany.
JensIn response to Jens Grochtdreis. Reply
Jens, at its simplest, browsers on Android seem to ignore the meta tag that signals a responsive page and instead treat the page width as the width of the table. I have been on the road, so only tested in fits and starts, but I would hope that a page with more content or layout context might reinforce that the browser should honor the .
Also, I failed to link the debug version of the pen above, which gets rid of the iframe and other CodePen cruft: cdpn.io/pen/debug/VwYyVJY
Reply
Hi,
a solution for a fixed header and the option to scroll vertically (for smaller screens) without a fixed height of the container div or better with no container is missing. I don’t know if it is possible but i think this is the most use use case for large tables.
In response to Alex. Reply
The first table under Examples has fixed column headers, no container, and no fixed height.
Reply
Is it possible to make the horizontal scrollbar stick to the bottom so the user doesn’t have to scroll to the bottom of the scrolling wrapper to access it?
In response to Tenba. Reply
Not stick, but you can give the container a height based on the viewport. Understand that you may then have two scrollbars.
Leave a Comment or Response Cancel response
- 3 June 2021: Some confirmation on the Twitters that Chrome 91 got the fix from the bug I cited, which you can confirm in this demo. It seems tables got a full re-write in Chrome.