TT Lab
Get started
Learn Learning paths Courses

CSS — The Later Rule Wins

The Cause Is Always an Ancestor

Continue in TT Lab

In one line

When fixed doesn't stay on the screen and sticky doesn't follow you, the cause is not that element but an ancestor. Find out who created the containing block and the scroll box, and you have the answer.

Why this was needed

For these two bugs, the symptom gives no hint of the cause at all.

In both cases that element's CSS is perfectly right. If you look at just that element in the developer tools, nothing is wrong. So people spend hours changing values. The place to look is the ancestor.

How it works — the containing block

Values like top, left, and width: 50% are all measured against the containing block. What that containing block is depends on the position value. MDN's containing block documentation sums up the rules.

position Containing block
static · relative The content area of the nearest block ancestor
absolute The padding area of the nearest positioned (not static) ancestor
fixed The viewport
sticky The parent for flow, and the nearest scroll box for what it sticks to

Most people know up to here. The problem comes next.

Properties that trap fixed

If any one ancestor has one of the properties below, that ancestor becomes the new containing block for fixed and absolute.

transform          (none 이 아닌 값)
filter
backdrop-filter
perspective
will-change: transform | filter | perspective
contain: layout | paint | strict | content

So this is what happens. You add transform: translateY(-2px) to give the card a slight lift effect on mouse hover. Or you add will-change: transform for performance. The moment you do, a modal overlay that was inside the card gets trapped inside the card. An animation and a modal look unrelated, so it takes a long time to find the cause.

CSS Positioned Layout 3 and CSS Containment 3 contain the respective rules.

Two reasons sticky doesn't stick

position: sticky works only when both conditions are met.

First, there must be a threshold. At least one of top, right, bottom, and left must not be auto. If there is none, there is no line to stick to, so it just flows along. There is no error and no warning.

Second, the scroll box must actually scroll. Sticky sticks only within the nearest scroll box. A scroll box is the nearest ancestor whose overflow is not visible.

Here overflow: hidden causes the accident. If you put overflow-x: hidden on an intermediate container to stop horizontal scrolling, overflow-y also becomes auto with it, and that box becomes a scroll box. The user does not scroll that box (they scroll the page), so sticky never gets a chance to stick.

And sticky's range of sticking is inside the parent box. If the parent is only as tall as itself, it follows you only that far. The symptom "it follows a little and then stops" is almost always this.

Inside which box does z-index compete

Sometimes you give z-index: 9999 and it still gets buried behind. It is not because the number is small but because it is inside a different stacking context.

Stacking contexts are created by the following. MDN's stacking context documentation gives the full list.

If an ancestor creates a stacking context, the z-index inside it works only within that context. If the parent is z-index: 1 and the sibling is z-index: 2, a child inside the parent is below that sibling even if it is 9999. A careless line like opacity: 0.99 creates this.

What it looks like in the field

Three things come up repeatedly.

First, a dropdown gets clipped by its parent. An ancestor has overflow: hidden and the dropdown is absolute. You can switch it to fixed, but if that ancestor has a transform, fixed is trapped too. The fundamental fix is to move the menu to the top of the document (a portal).

Second, the header doesn't stick only on mobile. There was a media query that put in overflow-x: hidden to prevent horizontal overflow on narrow screens.

Third, things are odd only on iOS. This is usually a problem with -webkit-overflow-scrolling or the safe area (env(safe-area-inset-*)), which is a different axis from the rules covered here, so you have to check on a real device. It is better not to fix it by guessing.

If you memorize the diagnosis order, most cases are done within 5 minutes.

  1. What is this element's position
  2. What is the containing block — go up the ancestors looking for the transform family
  3. If it is sticky, is there a threshold, and which one is the scroll box
  4. If it is z-index, which ancestor created a stacking context

What you will do in the next lab

You write a stylesheet for a given document structure and find the containing block and scroll box yourself with the calculator. You see with your own eyes how putting transform on a card traps the overlay, and how an ancestor's overflow: hidden kills sticky.