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
Note: after over one year without an answer, this question was also posted at Stack Overflow in Portuguese and - while still without a conclusive solution - some users and me were able to replicate the stacking mechanism in JavaScript (reinventing the wheel, but still...)
Quoting the stacking context algorithm at the CSS2 specification (emphasis mine):
The root element forms the root stacking context. Other stacking contexts are generated by any positioned element (including relatively positioned elements) having a computed value of 'z-index' other than 'auto'. Stacking contexts are not necessarily related to containing blocks. In future levels of CSS, other properties may introduce stacking contexts, for example 'opacity'
From that description, here's a function to return: a) the z-index
of an element, if it generates a new stacking contex; or b) undefined
if it doesn't>
function zIndex(ctx) {
if ( !ctx || ctx === document.body ) return;
var positioned = css(ctx, 'position') !== 'static';
var hasComputedZIndex = css(ctx, 'z-index') !== 'auto';
var notOpaque = +css(ctx, 'opacity') < 1;
if(positioned && hasComputedZIndex) // Ignoring CSS3 for now
return +css(ctx, 'z-index');
}
function css(el, prop) {
return window.getComputedStyle(el).getPropertyValue(prop);
}
This should be able to set apart elements that form different stacking contexts. For the rest of the elements (and for elements with an equal z-index
) the Appendix E says they should respect "tree order":
Preorder depth-first traversal of the rendering tree, in logical (not visual) order for bidirectional content, after taking into account properties that move boxes around.
Except for those "properties that move boxes around", this function shoud correctly implements the traversal:
/* a and b are the two elements we want to compare.
* ctxA and ctxB are the first noncommon ancestor they have (if any)
*/
function relativePosition(ctxA, ctxB, a, b) {
// If one is descendant from the other, the parent is behind (preorder)
if ( $.inArray(b, $(a).parents()) >= 0 )
return a;
if ( $.inArray(a, $(b).parents()) >= 0 )
return b;
// If two contexts are siblings, the one declared first - and all its
// descendants (depth first) - is behind
return ($(ctxA).index() - $(ctxB).index() > 0 ? a : b);
}
With these two functions defined, we can finally create our element comparison function:
function inFront(a, b) {
// Skip all common ancestors, since no matter its stacking context,
// it affects a and b likewise
var pa = $(a).parents(), ia = pa.length;
var pb = $(b).parents(), ib = pb.length;
while ( ia >= 0 && ib >= 0 && pa[--ia] == pb[--ib] ) { }
// Here we have the first noncommon ancestor of a and b
var ctxA = (ia >= 0 ? pa[ia] : a), za = zIndex(ctxA);
var ctxB = (ib >= 0 ? pb[ib] : b), zb = zIndex(ctxB);
// Finds the relative position between them
// (this value will only be used if neither has an explicit
// and different z-index)
var relative = relativePosition(ctxA, ctxB, a, b);
// Finds the first ancestor with defined z-index, if any
// The "shallowest" one is what matters, since it defined the most general
// stacking context (affects all the descendants)
while ( ctxA && za === undefined ) {
ctxA = ia < 0 ? null : --ia < 0 ? a : pa[ia];
za = zIndex(ctxA);
}
while ( ctxB && zb === undefined ) {
ctxB = ib < 0 ? null : --ib < 0 ? b : pb[ib];
zb = zIndex(ctxB);
}
// Compare the z-indices, if applicable; otherwise use the relative method
if ( za !== undefined ) {
if ( zb !== undefined )
return za > zb ? a : za < zb ? b : relative;
return za > 0 ? a : za < 0 ? b : relative;
}
else if ( zb !== undefined )
return zb < 0 ? a : zb > 0 ? b : relative;
else
return relative;
}
Here are three examples showing this method in practice: Example 1, Example 2, Example 3 (sorry, didn't bother translating everything to english... it's the exact same code, just different function and variable names).
This solution is most likely incomplete, and should fail in edge cases (though I couldn't find any myself). If anyone has any suggestions for improvements, it'd be really appreciated.
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!
You could get the elements' dimensions and offsets, and then use document.elementFromPoint() to determine which one is the element rendered on top.