← Single-axis scroll containers
v153 · css position · sticky
position: sticky per-axis resolution
How sticky positioning resolves its constraint independently per axis — against the nearest ancestor scroll container scrollable in that axis — which is what makes split-axis layouts (horizontally scrolling grid, vertically sticky header) work in plain CSS.
the rule (verbatim)
From CSS Positioned Layout Level 3 §3.4 (sticky positioning), fetched 2026-07-29:
“Sticky positioning is similar to relative positioning except the offsets are automatically calculated in reference to the nearest scrollport. For a sticky positioned box, the inset properties represent insets from the respective edges of the scrollport of the nearest scroll container with a matching scrollable axis, defining the sticky view rectangle used to constrain the box’s position. (The sticky view rectangle can be constructed from edges of two different scrollports, if the nearest scroll container is a single-axis scroll container.)”
For a position: sticky box the parenthetical is the behavioral core of this feature: when the nearest scroll container is single-axis, the sticky view rectangle’s edges may come from two different scrollports — the x edges from the single-axis container, the y edges from the next ancestor that scrolls in y. The per-axis insets (top, right, bottom, left) are measured against the scrollport found for their axis.
resolution walk-through
- For each axis, the UA walks the ancestor chain looking for the nearest scroll container with a scrollable axis matching that axis.
- An
overflow: clipaxis is not a scrollable axis — the WPT assertion states it directly: “sticky elements do not consideroverflow: clipcontainers as possible scroll ancestor”. The walk continues past it. - The two per-axis scrollports together define the sticky view rectangle; insets (
top,left, …) are measured from the respective edges of the scrollport found for that axis. - If both insets in a given axis are
auto, no offset is added in that axis — the element stays where it was laid out there (unchanged rule).
worked example: the split-axis data grid
<body> <!-- scrolls vertically (the page) -->
<div class="grid"> <!-- overflow: auto clip → single-axis (x) -->
<div class="header"> <!-- position: sticky; top: 0 -->
<div class="first-col"> <!-- position: sticky; left: 0 -->
| Element | Vertical (y) constraint resolves against | Horizontal (x) constraint resolves against |
|---|---|---|
.header (sticky top) | The page — .grid is not y-scrollable, so the walk continues past it | n/a (no x inset) |
.first-col (sticky left) | n/a (no y inset) | .grid — the nearest x-axis scroll container |
Result: the header follows vertical page scrolling while the grid scrolls horizontally beneath it; the first column stays pinned inside the grid’s horizontal scroll. Before single-axis scroll containers, .grid (via overflow-y: hidden) captured both constraints and the header never stuck to the page.
edge cases
- No scrollable ancestor in an axis — the constraint resolves against the initial containing block’s scrollport (the viewport), as before.
overflow: hiddenancestors — still count as scroll containers (programmatic scrolling), so they still capture sticky constraints in both axes; onlyclipopts an axis out. This asymmetry is the migration trap when converting oldhidden-based layouts.- RTL — covered upstream by the RTL WPT variants (
single-axis-overflow-clip-rtl.html,single-axis-scroll-into-view-rtl.html). - Both insets auto in an axis — no offset in that axis (per the spec paragraph above).
examples
.page { /* normal vertically scrolling document */ }
.grid {
overflow: auto clip; /* single-axis (x) scroll container */
max-width: 100%;
}
.grid thead th {
position: sticky;
top: 0; /* sticks to the PAGE, not .grid */
background: Canvas;
}
.grid td:first-child,
.grid th:first-child {
position: sticky;
left: 0; /* sticks inside .grid's horizontal scroll */
background: Canvas;
}
Source: CSS Position 3 §3.4; CSS Overflow 3 §3.1
compatibility
Same interim status as the overview table (runtime flag experimental at trunk, enable-by-default CL pending — see the overview ship-status note). No BCD key records per-axis sticky resolution.
| Browser | Per-axis sticky resolution |
|---|---|
| Chrome | Enabled by default 153 (per the listing); flag experimental at trunk |
| Edge | Not separately reported |
| Firefox | Unknown — Mozilla #1418 open, no signal |
| Safari | Unknown — WebKit #680 open, no signal |
security and privacy
No new exposure — a constraint-resolution change in layout. Per-axis resolution is observable via getBoundingClientRect() during scroll, which reveals engine/version family only, as all layout behavior does.