Two-Axis Sticky: Sticky Row and Column in the Same Table

Sticky first column and header row without JavaScript

Locking both the first column and the header row of a wide table used to require JavaScript scroll syncing or a heavy table library. A change to position: sticky lets it track different scrollers on each axis when the wrapping element only scrolls on one of them.

Old way 13 lines
const wrap = document   .querySelector('.table-wrap');const head = document   .querySelector('thead');window.addEventListener( 'scroll', () =>  {  const r = wrap         .getBoundingClientRect();  head.style.transform =       `translateY(${    Math.max(0,            -r.top)}px)`;});wrap.addEventListener(   'scroll', () =>  {  document.querySelectorAll(    '.first-col')      .forEach(el => {    el.style.transform =       `translateX(${wrap.scrollLeft}px)`;  });});
Modern
10 lines
.table-wrap   {  overflow-x: auto;  overflow-y: clip; }th:first-child,td:first-child   {  position: sticky;  left: 0;}thead th   {  position: sticky;  top: 0;}
Limited availability 0% global usage

This feature is not Baseline because it does not work in some of the most widely-used browsers.

Not ready for production without a fallback.

sticky first column tracks the wrapper, sticky header tracks the page
Item Q1 Q2 Q3 Q4
Alpha12182231
Bravo8111519
Charlie24283340
Delta591421
scroll horizontally inside the table

Two scrollers, one rule

The first column sticks against the table wrapper. The header row sticks against the document. No JavaScript scroll syncing, no transform math.

Single-axis scrollers

Setting overflow-y: clip on the wrapper means it only scrolls on one axis. Vertical stickiness falls through to the next scroller (the document).

Pair with scroll-state

Add container-type: scroll-state to style stuck headers and stuck columns differently when they engage. No JavaScript scroll observers.

Old Approach
JS scroll sync
transform tracking
Modern Approach
overflow-y: clip
Single-axis scroller
Status
Experimental
Behind a flag

How it works

Pin both the first column and the header row of a wide table and only one of them actually stays put. position: sticky historically tracked the nearest ancestor scroller. If that scroller only moved horizontally, vertical stickiness had nothing to track and the header scrolled away with the rest of the page. The fix was JavaScript: listen to window scroll, listen to wrapper scroll, and translate the header and the first column to fake the missing stickiness.

A recent change to the overflow spec lets a container scroll on only one axis. Set overflow-x: auto for horizontal scrolling and overflow-y: clip to opt out of vertical scrolling on the same element. The wrapper is now a single-axis scroller. The first column still sticks against the wrapper on the inline axis. The header row, having no vertical scroller to track at this level, walks up the ancestor chain and sticks against the document on the block axis. Two scrollers, one CSS rule.

Status: available behind the experimental web platform features flag in Chrome 148. Treat this as a preview, not a production pattern. Browser support data on this page will update once the flag is removed.

Accessibility note: Sticky columns hide content behind themselves when they overlap other cells. Make sure data in the sticky column has enough background contrast against the cells it covers, and that focus order still flows naturally for keyboard users navigating the table.
ESC