An element with position: fixed normally resolves against the viewport. Which properties on an ancestor element make that ancestor the containing block instead, and what is the visible effect?
answer
- fixed is not unconditionally viewport-anchored
- an ancestor can steal the anchor
- new coordinate space or containment
- transform, filter, perspective, contain, container-type
- stacking context is a different mechanism
basics
~20 sAn ancestor with transform, translate, rotate, scale, perspective, filter, backdrop-filter, will-change naming one of those, contain: layout/paint/content/strict, or a container-type becomes the containing block for fixed descendants. The fixed element then scrolls with that ancestor instead of staying put.
solid answer
~40 s`position: fixed` uses the viewport as its containing block only while no ancestor claims that role. Any ancestor with a `transform` — including the individual `translate`, `rotate` and `scale` properties — or with `perspective`, a non-`none` `filter` or `backdrop-filter`, `will-change` listing one of those, `contain: layout`, `paint`, `content` or `strict`, or a `container-type` other than `normal`, becomes the containing block instead. The fixed element then measures its insets from that ancestor and scrolls along with it, so `top: 0` means the top of that box rather than the top of the screen. The same properties also capture `position: absolute` descendants, which is why the spec describes them as creating a containing block for positioned descendants rather than as a fixed-positioning quirk.
code
css · 10 lines.shell {
transform: translateZ(0);
}
.shell .toolbar {
position: fixed;
top: 0;
left: 0;
right: 0;
}go deeper
Know the default first: a fixed element is positioned against the viewport and stays put while the page scrolls. Recognising that an ancestor can change this is enough at this level.
Name the properties that create a containing block for positioned descendants — transform and its individual properties, perspective, filter, backdrop-filter, will-change, contain — and explain that the fixed box then measures from that ancestor.
Show how you diagnose it in a real codebase: the fixed element did not change, an ancestor did, so you walk the chain checking computed styles for the containing-block-creating properties added for animation, effects or containment.
Own the structural rule — viewport-anchored UI should not live inside transformed or contained subtrees — and treat adding containment or transforms to shared wrappers as a change whose blast radius extends to descendants nobody is looking at.
## The default, and the exception `position: fixed` is defined as positioning against the **viewport**: the box is removed from flow, its insets are measured from the viewport edges, and it does not move when the page scrolls. That is the behaviour everyone expects and it holds in the vast majority of documents. The exception is a single rule with a list attached: if any ancestor is a containing block for fixed-positioned descendants, that ancestor's padding box replaces the viewport. The fixed element is then bounded by, and scrolls with, that ancestor — visually indistinguishable from an absolutely positioned element inside it. ## The list An ancestor takes that role when it has any of: - **`transform`** with a value other than `none`, or the individual transform properties **`translate`**, **`rotate`**, **`scale`** with a non-initial value. - **`perspective`** other than `none`. - **`filter`** other than `none`, or **`backdrop-filter`** other than `none`. - **`will-change`** naming any of the above (`will-change: transform` counts even though no transform is applied). - **`contain`** with `layout`, `paint`, `content` or `strict`, and `content-visibility` values that imply containment. - **`container-type`** other than `normal` — the container-query opt-in, which applies containment as part of its definition. Container queries reached broad browser support in 2023, so this is the newest entry on the list and the one people are least likely to have internalised. The first three have been specified and shipped for many years; treat them as universal. ## Why these properties and not others They all establish a coordinate system or a containment boundary that the element's descendants must respect. A transformed ancestor defines a new local coordinate space — a descendant painted "at the viewport top" inside a rotated box would have to escape that rotation, which the rendering model does not allow. Containment (`contain`, `container-type`) is a promise that the subtree's layout effects stay inside the box, and a fixed descendant escaping to the viewport would break that promise. Note what is *not* on the list. `opacity` less than 1, `isolation: isolate`, `mix-blend-mode` and `z-index` on a positioned element all create **stacking contexts**, which change paint order but do not create containing blocks. Stacking context and containing block are separate mechanisms, and a candidate who conflates them will make wrong predictions in both directions. ```css .animated-shell { transform: translateZ(0); /* or will-change: transform, or contain: paint */ } .animated-shell .toolbar { position: fixed; top: 0; left: 0; /* now the shell's padding edge, not the screen */ } ``` ## The symptom and the diagnosis The report is always some version of "it was fixed, and now it scrolls" — or the reverse, "it works on this page and not that one". The tell is that nothing about the fixed element itself changed; an ancestor did. Common culprits are a transform added for an animation or a GPU-promotion hack, a `filter` applied for a disabled or blurred state, `contain` or `content-visibility` added as a rendering optimisation, and `container-type: inline-size` added to make a component queryable. Each of those is added for reasons that have nothing to do with positioning, by someone who was not thinking about the fixed element three levels down. The diagnosis is mechanical: select the fixed element in devtools and walk up its ancestors checking computed values for `transform`, `filter`, `perspective`, `will-change`, `contain`, `content-visibility` and `container-type`. The first ancestor with one is the culprit. Nothing warns you at author time, and the element does not visually error — it simply positions against a different box. ## The absolute-positioning half The same properties create a containing block for `position: absolute` descendants too. For absolute that is rarely noticed, because a static ancestor being skipped or not skipped often changes little in a shallow tree — but it does mean a transformed wrapper silently becomes the anchor for an absolutely positioned child that was aiming at something further up. ## What this means for how you build The practical takeaway is a boundary rule: keep viewport-anchored elements out of subtrees that carry transforms, filters or containment, and treat adding any of those properties to a wrapper as a change with layout reach beyond that box. When a component's correctness depends on where in the tree it renders, that dependency should be explicit rather than discovered in a bug report.
- Does opacity: 0.5 on an ancestor have the same effect on a fixed descendant?No. `opacity` below 1 creates a stacking context, which changes paint order and z-index resolution, but it does not create a containing block. A fixed descendant inside a semi-transparent ancestor still resolves against the viewport. The two mechanisms are independent, and only some properties — notably `transform` and `filter` — do both.
- Does will-change: transform capture fixed descendants even when no transform is applied?Yes. `will-change` asks the browser to prepare for the named property, and preparing for `transform` means establishing the same containing block and stacking context the property itself would. That makes it a particularly sneaky cause, since the wrapper has no visible transform to notice.
- Do these properties also affect position: absolute descendants?Yes — the spec defines them as creating a containing block for *positioned* descendants, absolute included. An absolutely positioned child looking for an anchor stops at a transformed or contained ancestor even when that ancestor's `position` is `static`, so the same wrapper can silently re-anchor absolute children as well.
saying these in an interview costs you the question
- Believes position: fixed is always relative to the viewport, no exceptions
- Says opacity or z-index on an ancestor breaks fixed positioning
- Confuses creating a stacking context with creating a containing block
- Assumes only transform does this and misses filter, contain and container-type
- Thinks will-change: transform is inert until a transform is set