A horizontal strip inside a vertical page, with one label that has to stay pinned to the left of the strip and to the top of the viewport at the same time. That has never been possible in CSS, because an element with hidden overflow on one axis becomes a scroll container on both. One new value fixes it. Scroll this page and watch the two labels disagree.
overflow: scroll clip 0 scroll listeners ยท 0 layout readsoverflow-x: auto; overflow-y: hidden + a listener 0 events ยท 0 readsEvery one of them on the main thread, by definition: a scroll listener cannot run anywhere else.
getBoundingClientRect calls. Each one can force a synchronous layout.
Cumulative time inside the handler. Small per call, and it is per call.
There is no handler to time. The number is zero and stays zero.
A horizontal gallery needs its content clipped vertically, so you write overflow-y: hidden. That is the only sensible declaration available, and it has a consequence nobody wants: hidden is a scrolling overflow value. The box becomes a scroll container in the block axis too, one whose scrollable height happens to be zero.
Now put position: sticky; top: 78px on a child. Sticky resolves against the nearest scroll container in that axis, which is now the strip, which never scrolls vertically. So the property is honoured perfectly and does nothing. There is no warning, no invalid declaration, no devtools badge. It simply has no effect, which is why this costs an hour the first time.
The workaround is always the same shape and the right panel is running it: listen to the page scroll, read the strip's position with getBoundingClientRect, work out how far the label should be pushed down, write a transform. It works. It also puts a layout read and a style write on the main thread on every scroll event, on the one interaction where people notice jank first, and it has to be re-derived every time someone changes the layout.
/* Before. The y axis is clipped AND a scroll container. */ .strip { overflow-x: auto; overflow-y: hidden; } /* โ sticky top resolves against .strip, which never scrolls. Dead. */ /* Chrome 153. The y axis is clipped and NOT a scroll container. */ .strip { overflow: scroll clip; } /* โ sticky left resolves against .strip, sticky top against the page. */
From Chrome 153 a new stable ships every two weeks, which is the reason drop 051 exists and the reason nothing on this page says "needs Chrome 153+". It asks instead. Both probes below are one CSS.supports call and the exact string is shown next to the answer, so you can paste it into a console and get the same result.
๐ฉ The first row is a trap, and it is the detect everybody will reach for. Two-value overflow and the clip keyword have both parsed for years, so CSS.supports answers yes on browsers going back to Chrome 90, none of which have the behaviour. What changed in 153 is not the syntax, it is a computed-value rule: CSS Overflow 3 said that when one axis is clip and the other is a scrolling value, the clip computes to hidden. That coercion is what made the box a scroll container on both axes, and that coercion is what was removed.
So the detect has to look at the computed value or at the behaviour, not at the parser. Reading overflowY back tells you whether the coercion happened. Setting scrollTop and watching it refuse to move tells you the same thing from the other side, and it is the one this page uses to decide what to tell you, because it tests the thing that actually matters rather than a value that could in principle be reported differently.
scroll-axis-lock, for surfaces that genuinely move diagonallyThe other scroll item in the same release. Browsers constrain a scroll gesture to one axis, because on ordinary documents a slightly crooked swipe should go straight down rather than drifting sideways. On a map, a board, a timeline or a node graph that assumption is wrong, and until now the fix was to intercept the gesture and reimplement panning.
The surface below sets scroll-axis-lock: none. If your browser has it, a diagonal trackpad gesture moves diagonally. An honest caveat: this is a gesture behaviour, so a mouse wheel cannot demonstrate it, and neither can a script. There is no measurement on this page for this one, because there is nothing I can measure that would not be theatre. Try it with a trackpad or a touchscreen, or take it as a note for later.
The handler is cheap per call and the counters will show that honestly. The win is that the behaviour moves into the layout engine, where it is correct during momentum scroll, correct on the first frame, and correct when someone changes the header height without telling you.
It runs after the scroll, so it is a frame behind under fast input. Sticky is not: it is resolved during layout. This is the same one-frame lag that turns up between a smooth-scroll library and a WebGL canvas, in a smaller and more common place.
@supports (overflow: scroll clip) matches on browsers going back to Chrome 90, because the syntax has always parsed. Gating on it ships the CSS to machines that will coerce it and silently drops the listener they still need. Probe the behaviour once and set a class.
/* WRONG. This matches on Chrome 90, which does not have the behaviour. */ @supports not (overflow: scroll clip) { โฆ } /* Right: probe the computed value once, at boot, and set a class. */ const el = document.createElement('div'); el.style.cssText = 'position:absolute;left:-9999px;width:60px;height:50px;overflow:scroll clip'; el.appendChild(Object.assign(document.createElement('div'), { style: 'width:400px;height:400px' })); document.body.appendChild(el); el.scrollTop = 100; const singleAxis = el.scrollTop === 0; // 0 = clipped, not a scroller el.remove(); document.documentElement.classList.toggle('single-axis', singleAxis); /* Then gate both sides on the class, and the listener retires itself. */ .single-axis .strip { overflow: scroll clip; } html:not(.single-axis) .strip { overflow-x: auto; overflow-y: hidden; }