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 thannone)filter(values other thannone)backdrop-filter(values other thannone)contain(with values such aspaint,layout,strict, orcontent)will-change(specifying any property that would create a containing block when defined, such aswill-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.