Position Fixed Inside a CSS Transform

In CSS, applying a transform property to an ancestor element fundamentally alters how child elements with position: fixed behave. Instead of anchoring to the browser viewport as expected, the fixed element becomes trapped within the transformed ancestor, effectively behaving like an absolutely positioned element. This article explains the technical reason behind this behavior according to CSS specifications, identifies related properties that trigger the same effect, and provides practical solutions to resolve the issue.

The Mechanism

Under standard rendering rules, a position: fixed element uses the viewport (the initial containing block) as its reference frame. It remains stationary on the screen regardless of how far a user scrolls.

However, according to the CSS Transforms Module Level 1 specification, any value other than none on a transform property causes that element to become the containing block for all of its descendants, including those with position: fixed or position: absolute.

When an ancestor has a transform (such as transform: translateY(0) or transform: scale(1)), the browser establishes a local coordinate system for that ancestor. Because the child element must render within this transformed coordinate space, the viewport can no longer act as its containing block. As a result:

  • The fixed element calculates its coordinates (top, right, bottom, left) relative to the transformed ancestor’s padding box.
  • If the page scrolls and the transformed ancestor moves off-screen, the "fixed" element scrolls away with it.

Other Properties That Trigger This Behavior

The transform property is not the only CSS declaration that creates a new containing block for fixed elements. The same behavior occurs when an ancestor uses:

  • perspective (values other than none)
  • filter (values other than none)
  • backdrop-filter (values other than none)
  • contain (with values such as paint, layout, strict, or content)
  • will-change (specifying any property that would create a containing block when defined, such as will-change: transform)

How to Fix the Issue

To restore the standard viewport-relative behavior of a position: fixed element, use one of the following approaches:

1. Move the Fixed Element Outside the Transformed Container

The most reliable solution is to place the fixed element outside the DOM hierarchy of the transformed ancestor. Moving overlays, modals, and sticky navigation bars directly under the <body> element ensures their containing block remains the viewport. In component-based frameworks like React, Vue, or Angular, this is commonly handled using Portals or Teleports.

2. Remove the Transform from the Ancestor

If the transform is not essential, replace it with alternative layout properties. For example, if transform: translate(-50%, -50%) is used strictly for centering, you can switch the parent layout to CSS Grid or Flexbox:

/* Replace transform centering */
.parent {
  display: flex;
  align-items: center;
  justify-content: center;
}

3. Animate Inner Elements Instead

If transforms are used for animations, apply the transform property to a sibling or an inner wrapper rather than an ancestor that encapsulates fixed-position children. Isolating transformed elements prevents unintended changes to the containing blocks of parent or sibling elements.