Recording of the demo.

~

If you’ve ever tried to build a data table with a sticky header and a sticky first column, you know the pain. You’d think a simple position: sticky with top: 0 and left: 0 would be enough, but the reality was that only one of both would stick.

A recent change to CSS fixes this: position: sticky now plays nice with single-axis scrollers, allowing you to have sticky elements that track different scroll containers on different axes. This change is available in Chrome 148.

~

# The Situation

To understand the impact, let’s quickly review the standard setup for a responsive table. You start with a wide HTML <table> full of data. To prevent this wide table from breaking your page layout on small screen devices, you typically wrap it in a container with overflow-x: auto.

<div class="table-wrapper" style="overflow-x: auto;">
  <table>
    …
  </table>
</div>

Because you want the column headers (the <thead>) to stay visible when scrolling down the document, you apply position: sticky; top: 0; to them. Simultaneously, you want your first column to stay visible when scrolling sideways through the data, so you apply position: sticky; left: 0; there.

.table-wrapper {
  overflow-x: auto;
}

.table-wrapper thead {
  position: sticky;
  top: 0;
}

.table-wrapper td:first-child {
  position: sticky;
  left: 0;
}

However, this doesn’t work as expected. While the first column will stick to the left edge of the table wrapper when scrolling horizontally, the headers will scroll out of view along with the rest of the page.

This is because the .table-wrapper becomes the sticky reference for both axes. Because the wrapper only scrolls horizontally, the vertical sticking becomes completely ineffective. Your top: 0 headers would just happily scroll out of view along with the rest of the page 😭

To get around this, you had to rely on lots of JavaScript to synchronize the scroll position, or rely on duplicated headers.

Note: While there are pure CSS solutions to make sure the headers also stick within the .table-wrapper, the problem described here is different: instead of getting the headers to stick inside the same scroller, you want the headers to be stuck against a different scroller (here: the .table-wrapper for horizontally sticky stuff, and the document’s scroller for vertically sticky stuff).

~

# The Change

A recent change to CSS fixes this issue: position: sticky can now track the nearest scrolling container per axis.

This means that for our wide data table:

No more duplicating headers. No more scroll-syncing JavaScript. Just plain CSS doing exactly what you’d expect it to do.

The request for this change was filed back in 2017, in CSS WG Issue #865. It’s not that often you see three-digit issues get resolved and implemented πŸ˜…

~

# See it in action

You can see this new behavior in action in this CodePen demo:

See the Pen
CSS `position: sticky` for Single Axis Scroll Containers
by Bramus (@bramus)
on CodePen.

If you view the demo in a supported browser, the first column will stick to the left edge of the table wrapper when scrolling horizontally, and the top headers will stick to the top of the viewport when scrolling vertically.

~

# An Important Detail: Watch your overflow

If you look closely at the CSS in the demo, you might notice a very specific detail in how the .table-wrapper is styled.

.table-wrapper {
  overflow: auto clip;
}

To make the wrapper scroll horizontally, you might instinctively reach for overflow-x: scroll (or auto). However, doing so will actually break the vertical stickiness we just achieved!

Why? Because in CSS, if you set overflow-x to scroll or auto, the browser automatically computes overflow-y to auto as well (assuming its value was visible). This turns the wrapper into a scroll container on the vertical axis too, which means it once again traps our headers and becomes their sticky reference.

To fix this, we need to explicitly tell the browser not to create a scroll container on the block axis. We do this by using the clip keyword:

.table-wrapper {
  /* ❌ This computes overflow-y to auto, breaking vertical stickiness */
  /* overflow-x: auto; */

  /* βœ… This creates a scroller on the inline axis, but clips the block axis */
  overflow: auto clip; 
}

By setting overflow-y to clip (via the overflow shorthand), the wrapper does not become a scrollport on the vertical axis. The headers are therefore free to track the document viewport instead.

~

# Browser Support

πŸ’‘ Although this post was originally published in March 2026, the section below is constantly being updated. Last update: March 30, 2026.

At the time of writing, this feature is only supported in Chrome 148

Chromium (Blink)

βœ… Available in Chromium 148.0.7742.0.

Firefox (Gecko)

❌ No support

Subscribe to Bug #2023702 for updates.

Safari (WebKit)

❌ No support

~

# Feature Detection

Because older browsers still use the inner container as the sticky reference for both axes, you might want to feature-detect this new behavior to provide JS-based fallbacks or conditionally load specific styles.

While we can’t easily detect this layout behavior directly with an @supports query in CSS yet β€” I have an open issue at the CSSWG for this β€” you can feature detect this using JavaScript. The CodePen demo linked above includes a script that does exactly this.

It handles the detection by evaluating how the browser resolves the sticky positioning offsets within a scrolling wrapper. Feel free to peek at the JS tab in the Pen to grab the snippet for your own projects.

~

# Spread the word

Feel free to reshare one of the following posts on social media to help spread the word:

~

πŸ”₯ Like what you see? Want to stay in the loop? Here's how:

I can also be found on 𝕏 Twitter and 🐘 Mastodon but only post there sporadically.