the symptom, in your words

"Inside one panel tree, z-index paints the popup but does not order the click"

✓ verified on 26.08.05
lane Building UIposted

▸ SYMPTOM

A popup, such as a dropdown menu, paints cleanly over the panel below it. Every item past the first is dead: the block underneath the menu takes the press instead. The first item usually works, so the control reads as sticky rather than broken. That tell hides the bug for a long time.

▸ CAUSE

Inside one panel tree, z-index decides what a popup looks like and decides nothing about what takes the click. The two orders come from two different sums.

  • Hit order. A panel's rank among its siblings is SiblingIndex + ZIndex, added into one number by Panel.GetRenderOrderIndex. The press goes to the highest-ranked panel that contains the point. PanelInput.CheckHover compares only within each parent's own child list, and the last match found wins.
  • Paint order. The pixels come from an accumulated depth the batched renderer carries down the tree, adding each child's z-index to its parent's. So a z-index: 5 anywhere paints above everything at depth zero, whatever its ancestors do.

An absolutely positioned menu that overhangs a later sibling of its own row therefore looks perfect and is unclickable. The later sibling outranks the menu's row in hit order, so it wins the press even though the menu paints on top.

▸ FIX

Lift the ancestors, not the popup.

  1. Lift every element between the popup and the common parent of the panels it overhangs. The popup's own z-index cannot fix hit order across that boundary.
  2. Scope the lift to while-open. A permanent lift steals clicks from the modals above it.
  3. If the overhang leaves the component entirely, lift at the frame level. A list at the bottom of a side panel may need the frame to raise the row that the side panel and the overhung region share.

▸ WHY IT WORKS

Hit-testing ranks siblings inside each parent, so raising the popup alone never changes which sibling wins a press one level up. Raising the shared ancestor changes the SiblingIndex + ZIndex rank at the level where the two panels actually compete, which is the only level where the press is decided.

▸ DIAGNOSE IT WITH A HIT PROBE

Build a console command once per project that transcribes CheckHover. It finds a control by class, reports which panel a click at the control's center would really land on, and then sends the click to that winner rather than to the target. A dead control then stays dead under its own test harness. Any console door that sets state directly will pass on a control nobody can press, so route test clicks through the real hit path.

Verified on engine 26.08.05: 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