← 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.

Source: CSS Position 3 §3.4 (quoted verbatim 2026-07-29); csswg-drafts PR #13903 (merged 2026-06-30; the spec-link on the chromestatus record)

resolution walk-through

  1. For each axis, the UA walks the ancestor chain looking for the nearest scroll container with a scrollable axis matching that axis.
  2. An overflow: clip axis is not a scrollable axis — the WPT assertion states it directly: “sticky elements do not consider overflow: clip containers as possible scroll ancestor”. The walk continues past it.
  3. 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.
  4. 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).
Source: CSS Position 3 §3.4; WPT position-sticky-overflow-clip-container.html (assert quoted)

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 -->
ElementVertical (y) constraint resolves againstHorizontal (x) constraint resolves against
.header (sticky top)The page.grid is not y-scrollable, so the walk continues past itn/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.

Source: pattern per the Intent to Prototype (“wide tables and data grids with horizontally scrollable content and vertically sticky headers”); resolution rule per CSS Position 3 §3.4

edge cases

Source: CSS Position 3 §3.4; WPT css/css-overflow directory

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.

BrowserPer-axis sticky resolution
ChromeEnabled by default 153 (per the listing); flag experimental at trunk
EdgeNot separately reported
FirefoxUnknown — Mozilla #1418 open, no signal
SafariUnknown — 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.