the symptom, in your words

"ScreenPanel.ZIndex orders paint only — pointer input falls through"

✓ verified on 26.07.15a
lane Building UIposted

▸ SYMPTOM

A full-screen modal renders correctly on top of other UI (its ScreenPanel.ZIndex is higher), and some of its controls work. But clicks in regions where the lower root also has hit area trigger the underlying screen's buttons instead of the modal's. The modal looks broken — it draws correctly but cannot receive input in overlapping regions.

▸ CAUSE

ScreenPanel.ZIndex controls paint order only. Pointer input routing does NOT consult cross-root z-ordering. Each ScreenPanel root maintains its own independent hit-test surface, and a lower-ZIndex root whose panel has pointer-events: all keeps intercepting pointer events in its active region regardless of what is drawn on top of it.

This means a higher-ZIndex modal's own pointer-events: all scrim cannot block the lower root's input. The two roots' hit surfaces are parallel and independent — painting one on top of the other does not suppress the one underneath.

▸ FIX

Use cooperative de-arming instead of relying on z-order for input:

  1. The lower root must drop its own hit surface while any modal that can cover it is open. Add a computed class (e.g. under-modal) that sets pointer-events: none on the lower panel, keyed on every covering modal's open flag.

  2. Those modal open flags must be part of the lower panel's BuildHash, or the class toggle never re-evaluates and the stale pointer-events: all persists.

  3. Every new full-screen modal added over an armed full-screen surface must join that surface's de-arm condition. The modal's own scrim cannot protect it — only the lower root removing itself from the input surface does.

▸ WHY IT WORKS

The engine processes each ScreenPanel as an independent input root. There is no global z-ordered input compositor that would let a higher panel shadow a lower one's hit tests. By having the lower root voluntarily remove itself from the input surface (pointer-events: none), the modal's own hit surface becomes the only active one in its region — input routes correctly without requiring the engine to implement cross-root z-aware input dispatch.

The BuildHash requirement is the subtle part: razor panels only re-evaluate their tree when the hash changes, so a flag that controls pointer-events but is not part of BuildHash creates a stale-tree bug where the class never updates even though the modal state changed.

Verified on engine 26.07.15a: seen in a real project.
s&box moves fast; an undated fix is a liability. Spot a stale detail?
changelog
  • Published

Want to know when new guides or fixes drop? Join the community to help build this out. Report gotchas, flag outdated fixes, or just lurk.

Join the Discord