Search code examples
htmlcssz-indexwebpage-rendering

Compare HTML elements by actual z-index


Given two abitrary HTML elements A and B in the same document, how can I find out which one is "closer" to the user (i.e. if they overlap, which one is obscuring the other)?

The W3C CSS Specification describes stacking contexts, which compliant rendering engines should implement. However, I couldn't find a way to access this information in a JavaScript program, cross-browsers or not. All I can read is the css z-index property, that per se doesn't say much, since most of the time is set to auto or, even when expressed as a numeric value, is not a reliable indicator of how it's actually displayed (if they belong to different statcking contexts, comparing z-indexes is irrelevant).

Please note that I'm interested in arbitrary elements: if both elements are below the mouse pointer, only one will be considered "hovered", so I can easily find the closest one in this case. However, I'm looking for a more general solution, preferably one that does not involve re-implementing the stacking algorithm that the rendering engine is already performing.

Update: let me clarify a bit the reason behind this question: I recently tackled a question that exposed a limitation in jQuery's drag and drop mechanism - it doesn't take z-indexes into account when dropping, so if an element is obscuring another, it can still perform the drop operation in the element that is "behind". While the linked question was answered for the OP particular case, the general problem persists, and there's no easy solution that I know of.

alex's answer below is useful, but not enough for the case at hand: when dragging, the dragged element itself (or more precisely its helper) is the topmost element under the mouse cursor, so elementFromPoint will return it instead of the next topmost element, that we really need (workaround: style the cursor so it's placed outside the helper). The other intersection strategies that jQuery employ also take into account more than just one point, complicating the task of determining the topmost element that intersects the helper somehow. Being able to compare (or sort) elements by actual z-index would make a "z-index aware" intersection mode viable for the general case.


Solution

  • After some days of research I think I've successfully re-implemented the stacking mechanism according to the rules of 2016. I've basically updated the 2013 approach (posted by the OP). The result is a function which compares two DOM nodes, and returns the one which is visually on top.

    front = $.fn.visuallyInFront(document.documentElement, document.body);
    // front == <body>...</body> because the BODY node is 'on top' of the HTML node
    

    Reasoning

    There are other ways to determine which element is on top of the other. For example document.elementFromPoint() or document.elementsFromPoint() spring to mind. However, there are many (undocumented) factors that influence the reliability of these methods. For example, opacity, visibility, pointer-events, backface-visibility and some transforms may make document.elementFromPoint() unable to hit test a specific element. And then there is the issue that document.elementFromPoint() can only query the top-most element (not underlying ones). This should be solved with document.elementsFromPoint(), but currently has only been implemented in Chrome. In addition to that, I filed a bug with the Chrome developers about document.elementsFromPoint(). When hit testing an anchor tag, all underlying elements go unnoticed.

    All these issues combined made me decide to attempt a re-implementation of the stacking mechanism. The benefit of this approach is that the stacking mechanism is documented quite extensively and that it can be tested and understood.

    How it works

    My approach re-implements the HTML stacking mechanism. It aims to correctly follow all the rules which influence the stacking order of HTML elements. This includes positioning rules, floats, DOM order but also CSS3 properties like opacity, transform and more experimental properties like filter and mask. The rules seem to be correctly implemented as of march 2016, but will need to be updated in the future when the specification and browser support changes.

    I've put everything together in a GitHub repository. Hopefully this approach will continue to work reliably. Here is an example JSFiddle of the code in action. In the example all elements are being sorted by actual 'z-index', which is what the OP was after.

    Testing and feedback on this approach would be very welcome!