Skip to content

Hit-testing misses position: fixed elements with transform when nested inside a non-stacking-context parent - #618

Open
jerry4718 wants to merge 2 commits into
DioxusLabs:mainfrom
jerry4718:fix/hoisted-content-area-stale-layout
Open

Hit-testing misses position: fixed elements with transform when nested inside a non-stacking-context parent#618
jerry4718 wants to merge 2 commits into
DioxusLabs:mainfrom
jerry4718:fix/hoisted-content-area-stale-layout

Conversation

@jerry4718

@jerry4718 jerry4718 commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Problem

A position: fixed element with transform: translate(-50%, -50%) (centered modal) nested inside a full-screen position: fixed wrapper is not hit-testable. Clicks land on the wrapper instead of the modal content.

This affects any structure like:

<body style="margin: 8px">  <!-- any non-zero margin/offset -->
  <div class="mask-content" style="position: fixed; width: 100vw; height: 100vh; z-index: 1001">
    <div class="modal-content" style="position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 1001">
      <button>Cancel</button>
    </div>
  </div>
</body>

The bug is especially reproducible when the modal is dynamically inserted (e.g. Vue v-if) after initial render, triggering the incremental layout path.

Root causes

1. Fixed-position hoisted children get incorrect parent layout offset

In flush_styles_to_layout_impl (packages/blitz-dom/src/layout/damage.rs), when a hoisted child is passed up from a non-stacking-context parent to the parent's stacking context, the parent's final_layout().location is added to the child's hoisted position:

hoisted.position.x += position.x - scroll_offset.x as f32;
hoisted.position.y += position.y - scroll_offset.y as f32;

For position: fixed elements this is wrong. They are positioned relative to the viewport, not the parent's content box. If <body> has a margin (e.g. 21px from default user-agent styles or margin collapsing), this offset is incorrectly applied to the fixed element, shifting all hit-test coordinates by that amount.

When the modal is small enough that the offset pushes the click coordinate outside the modal's transformed bounds, the modal becomes unclickable.

Fix: Skip adding the parent layout offset for position: fixed hoisted children.

2. content_area does not account for transforms on hoisted children

compute_content_size runs during flush_styles_to_layout, which executes before resolve_layout and resolve_transforms. At that point final_layout() returns zeros and transforms are not yet computed, so content_area is Rect::ZERO. This means matches_hoisted_content in hit_inner is always false for any stacking context whose children were flushed in the same pass.

Even after layout resolves, content_area was computed from pre-transform final_layout().location. A hoisted child with transform: translate(-50%, -50%) may be visually present at a point outside its untransformed content_area, causing the matches_hoisted_content gatekeeper to incorrectly skip it.

Fix: Added recompute_stacking_context_content_area called after resolve_transforms in resolve.rs. It recomputes content_area using transform-adjusted corner positions (applying Affine::translate(layout_location) * transform to the child's border box corners), so the gatekeeper correctly reflects where hoisted children are actually rendered.

Reproduction

// Test: dynamically_inserted_modal_content_is_hittable
// 1. Render page with empty mask-content
// 2. Pump (resolve layout)
// 3. Dynamically insert modal-content into mask-content
// 4. Pump again (incremental layout)
// 5. Hit-test at center of viewport -> should hit modal content, not mask-content

Without the fixes, step 5 hits mask-content instead of modal-content.

WPT results

No changes in test results compared to main.

Generated by the WPT workflow.

Recompute stacking context content areas after layout and transform resolution.
The earlier calculation ran before final layouts existed and ignored CSS transforms,
so hit-testing could skip transformed hoisted children.
Fixed-position elements are positioned relative to the viewport, but hoisted
children inherited their parent's layout offset while being propagated to the
parent stacking context. A body margin or other ancestor offset could therefore
shift fixed overlays and cause hit-testing to miss nested modal content.

Skip the parent layout and scroll offset when propagating fixed-position hoisted
children. Keep the parent offset behavior for absolute and other positioned
elements whose coordinates depend on the parent context.

Tests:
- dynamically_inserted_modal_content_is_hittable
- dynamically_inserted_modal_content_hittable_at_transformed_edge
@jerry4718
jerry4718 force-pushed the fix/hoisted-content-area-stale-layout branch from d5c7c71 to aaa5f53 Compare August 6, 2026 05:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant