:hover, its ancestors do not —pointerenter/pointerleave still fire on those ancestorsTwo specifications describe the same pointer position over the same unchanged DOM tree, and give
opposite answers to "is the pointer inside #outer?".
The left table below is computed by this page directly from the two specification rules, so it is what the specs require no matter which browser you are reading this in. The right table is what this browser actually reports. They are separate on purpose: the argument here is between two specs, and any disagreement between the tables is a separate, additional matter.
One box, always at the same place on screen and always a DOM child of #mid. The button changes
one thing: whether it is in the
top layer.
Hit-test target: none
Legend
:hover.
Drawn by a plain :hover rule, so it is the browser's own answer with no script involved.pointerenter and has not yet received pointerleave, so the events consider the
pointer to be inside it. A box with an orange ring and no blue outline is the inconsistency.
(body and html have no box drawn; see the tables.):hover answer
for that box differs from what the specifications require. Nothing to do with the argument on this page; in
a conforming browser it never appears. The right-hand table's last column says the same thing in words.Look for rows where :hover says
no while the other two say yes. Those
are the ancestors above the top layer element.
Should be identical to the table on the left.
Event log — pointerenter/pointerleave
do not bubble, so each line is a separate dispatch to that node.
The whole inconsistency fits in two nearly identical loops. One has a break; the other does not.
These are the functions that produce the left-hand table above.
Being in the top layer does not change the tree. It only changes where boxes are generated:
Documents have a top layer, an ordered set containing elements from the document.
Elements in the top layer do not lay out normally based on their position in the document; instead they
generate boxes as if they were siblings of the root element.
CSS Positioned Layout 4 § Top Layer —
drafts.csswg.org/css-position-4/#document-top-layer
Rule A — CSS truncates at the top layer.
Some user action pseudo-classes, in addition to matching on the particular element with the property in question, match on that element's ancestors as well::hover,:active,:focus-within. Specifically, if these match on a given element, they also match on the element's flat tree ancestors, up to and including the first top layer element or the root element, whichever is encountered first. Selectors 4 § User Action Pseudo-classes — drafts.csswg.org/selectors-4/#useraction-pseudos
Note that Selectors 4 § :hover itself states the descendant rule flatly —
"an element also matches :hover if one of its descendants in the flat tree […] matches the
above conditions" — with no top layer clause. The truncation lives only in the parent section.
Rule B — Pointer Events does not truncate. The boundary events are defined over the plain DOM ancestor chain, and no clause anywhere mentions the top layer:
The user agent MUST fire a pointer event namedpointerenterwhen […] the pointer has moved into an element or one of its descendants. […] andpointerleavewhen […] the pointer has moved out of an element and all of its descendants. Pointer Events § 5.3.2 / § 5.3.9 — #the-pointerenter-event, #the-pointerleave-event
2. Let target be hit test with viewport-relative coordinates from native
3. Let targetDomPath be inclusive ancestors of target
[…] Let enterElements be a copy of targetDomPath with all elements common to last mouse DOM path removed. For each element in enterElements […] maybe send pointerenter event Pointer Events § 4.2.17 handle native mouse move — #handle-native-mouse-move. Inclusive ancestors is DOM's plain node-tree concept.mouseenter/mouseleaveare the very same steps, so they behave identically. The section carries a "needs to be revised" warning and w3c/pointerevents#285, but it is the only normative dispatch text there is.
The inconsistency. With the box in the top layer, hovering it means:
#pop matches :hover, and propagation stops there, so #mid,
#outer, body and html do not match :hover.#mid, #outer, body, html are on
targetDomPath, so all of them receive pointerenter and none of them has received
pointerleave.Both rules start from the same hit-test target, and neither spec references the other's answer about the ancestors of that target.
These exact strings are what the page injects and runs. The button does one thing to
#pop: setAttribute("popover","manual"); showPopover() to put it in the top layer, and
hidePopover(); removeAttribute("popover") to take it out again.
:has(:hover) column above. #outer:hover does not match while
#outer:has(:hover) does: #pop matches :hover and is still a descendant
of #outer in the tree. Rule A only constrains where :hover propagates, and
:has() is defined over the tree, which the top layer does not change. So this is not even a
disagreement between two specs — it is two selectors in Selectors 4 giving opposite answers about one
element. Every row above the top layer element shows both disagreements at once: :hover no,
:has(:hover) yes, pointer inside yes.pointerover/mouseover bubble, so a delegated handler on #outer fires
while #outer:hover is false. Likewise :focus-within truncates at the top layer while
focusin bubbles to the root, and :active truncates while pointerdown
bubbles.The practical failure mode: a menu kept open by .menu:hover in CSS and by a
pointerenter/pointerleave hover-intent handler in JS. Move the submenu into a popover
and the CSS half silently stops applying to the ancestor while the JS half keeps working. The same applies to
<dialog>.showModal() and requestFullscreen(), which also use the top layer.