assailant
assailant

Reputation: 75

hover, z-index and layering... a convoluted mess?

I'm a web developer noob and hoping someone with more experience can help.

<div id="domain-background-container">
  <div class="domain-hotspots">
    <div id="hotspot">
      <p class="bubble">I'm red on hover</p>
    </div>
  </div>
</div>

<div id="page-nav" >
  <p>I'm red on hover</p>
</div>
html, body {      /* <--- (1) */
  height: 100%; }

#domain-background-container {
  position: fixed;
  z-index: -10; }  /* <--- (2) */

.domain-hotspots {
  width: 100vw;
  height: 100vh;
  position: fixed; 
  background: blue; }

  #hotspot {
    width: 2em;
    height: 2em;
    background: green; }

  #hotspot:hover .bubble {
    background: red; }

#page-nav {
  width: 100%;
  text-align: center;
  background: yellow; }

#page-nav:hover p {
  color: red; }

Here's the jsfiddle.

The problem is that #hotspot (that is the green square) doesn't enter the hover state when the mouse cursor is placed over it.

I'm looking to have four things explained:

  1. Can someone provide or point me to a detailed explanation of the mechanics preventing #hotspot from entering the hover state?
  2. Removing CSS ruleset (1) [height: 100% for html and body] allows #hotspot to enter the hover state. This isn't a viable solution in my project, but for curiosity's stake, why does this make a difference.
  3. Removing the the z-index of CSS ruleset (2) also allows #hotspot to enter the hover state. Unfortunately, this also screws up z-ordering as .domain-hotspots (blue) then covers #page-nav (yellow) which isn't acceptable. Theoretically I could position and raise the z-value of #page-nav. While that would work for this simple jsfiddle I'm loath to do that for a bigger project where I'd have to apply that to every element following .domain-hotspots (which shouldn't even be necessary, because to my understanding elements with unmodified z-index which follow .domain-hotspots in the html file should be rendered on top of .domain-hotspots in the viewport) :-/
  4. I can't use the two "solutions" I just mentioned, so what solution remains?

I'd really appreciate any knowledge people can share on this! I'm stumped. I'm not even sure what I should google for to research the issue, as I don't know what's causing this behaviour.

Upvotes: 6

Views: 263

Answers (3)

Jacob
Jacob

Reputation: 78870

The rules governing how this all works is a bit complex; if you really want to look into it, see Appendix E. Elaborate description of Stacking Contexts from the CSS spec. As to your specific questions:

  1. The hotspot is not triggered because #domain-background-container is at a negative z-index, so the body, which has the default z-index, covers it and "blocks" the pointer events. You can see this in action if you go into the developer tools and use the "click to select an element" feature; the body is highlighted instead of the hotspot because it's on top.

  2. If you remove the height of the body, because the body only contains positioned elements, the body's natural height is now 0, so it no-longer blocks the background. An element only has height if you give it one or if it contains "flowing" elements (not absolute or fixed) that lend it height.

  3. Yes, playing the z-index game does not scale well in a project; keep it to a minimum and try to use natural stacking whenever possible.

  4. It's not clear what your intention is with your page's layout, but there are multiple potential solutions. You can use pointer-events: none on elements where you want the mouse events to "pass through" to a lower level, for example.

Sample:

body {
  pointer-events: none;
}

#domain-background-container {
  pointer-events: auto;
}

Upvotes: 4

Siguza
Siguza

Reputation: 23870

Can someone provide or point me to a detailed explanation of the mechanics preventing #hotspot from entering the hover state?

<body> is covering it up.
This can be seen by using the "Inspect element" function of a browser's dev tools.
(Due to the way z-index works though, giving it a background does not put that background in front of the div.)

Removing CSS ruleset (1) [height: 100% for html and body] allows #hotspot to enter the hover state. This isn't a viable solution in my project, but for curiosity's stake, why does this make a difference.

With height: 100%:

screenshot

Without height: 100%:

screenshot

[...] to my understanding elements with unmodified z-index which follow .domain-hotspots in the html file should be rendered on top of .domain-hotspots in the viewport.

From the CSS Level 2 draft, § 9.9.1:

Within each stacking context, the following layers are painted in back-to-front order:

  1. the background and borders of the element forming the stacking context.
  2. the child stacking contexts with negative stack levels (most negative first).
  3. the in-flow, non-inline-level, non-positioned descendants.
  4. the non-positioned floats.
  5. the in-flow, inline-level, non-positioned descendants, including inline tables and inline blocks.
  6. the child stacking contexts with stack level 0 and the positioned descendants with stack level 0.
  7. the child stacking contexts with positive stack levels (least positive first).
  • #page-nav is category 3 due to the default display: block on any <div>.
  • #domain-background-container is category 2 due to position: fixed, which removes it from the flow and creates a child stacking context, and due to its negative z-index.

Category 3 gets rendered over category 2, so #page-nav is in front of #domain-background-container.
Now when you remove the z-index from #domain-background-container, or make it 0 or positive, it becomes category 6 or 7, thus being rendered after (i.e. over) #page-nav.

Theoretically I could position and raise the z-value of #page-nav. While that would work for this simple jsfiddle I'm loath to do that for a bigger project where I'd have to apply that to every element following .domain-hotspots. [...] I can't use the two "solutions" I just mentioned, so what solution remains?

You don't actually have to raise its z-value, you merely have to position it. position: relative will do, even.
And applying that to every element following #domain-background-container is no miracle either:

#domain-background-container ~ * {
    position: relative;
}

Works quite well:

html, body {      /* <--- (1) */
  height: 100%; }

#domain-background-container {
  position: fixed;
  /*z-index: -10;*/ }  /* <--- (2) */

#domain-background-container ~ * {
  position: relative; }

.domain-hotspots {
  width: 100vw;
  height: 100vh;
  position: fixed; 
  background: blue; }

  #hotspot {
    width: 2em;
    height: 2em;
    background: green; }

  #hotspot:hover .bubble {
    background: red; }

#page-nav {
  width: 100%;
  text-align: center;
  background: yellow; }

#page-nav:hover p {
  color: red; }
<div id="domain-background-container">
  <div class="domain-hotspots">
    <div id="hotspot">
      <p class="bubble">I'm red on hover</p>
    </div>
  </div>
</div>

<div id="page-nav" >
  <p>I'm red on hover</p>
</div>

Admittedly though, there are situations where it is not feasible to position all those elements.

Another solution though, is to simply position the body element, which then allows you to give it a z-index of its own:

body {
    position: relative;
    z-index: -11;
}

Demo:

html, body {      /* <--- (1) */
  height: 100%; }

body {
    position: relative;
    z-index: -11; }

#domain-background-container {
  position: fixed;
  z-index: -10; }  /* <--- (2) */

#domain-background-container ~ * {
  position: relative; }

.domain-hotspots {
  width: 100vw;
  height: 100vh;
  position: fixed; 
  background: blue; }

  #hotspot {
    width: 2em;
    height: 2em;
    background: green; }

  #hotspot:hover .bubble {
    background: red; }

#page-nav {
  width: 100%;
  text-align: center;
  background: yellow; }

#page-nav:hover p {
  color: red; }
<div id="domain-background-container">
  <div class="domain-hotspots">
    <div id="hotspot">
      <p class="bubble">I'm red on hover</p>
    </div>
  </div>
</div>

<div id="page-nav" >
  <p>I'm red on hover</p>
</div>

Upvotes: 5

Dekel
Dekel

Reputation: 62596

  1. By setting the #domain-background-container to z-index: -10, you actually put that div behind the body tag, so you hover on the body tag and not on the #hotspot tag.
  2. By removing the 100% height on the body & html elements, the body element's height will actually be height of the #page-nav element (because the other element is positioned as fixed, so it will not take height of the page) - hence - you can hover it, because there is no other element that sits "before" it - z-index based.
  3. Not sure if there is a question here (?)
  4. You solution can be to set the position of #nav-page to relative and remove the z-index on the #domain-background-container.

Here is an example:

html, body {
  height: 100%; 
}

#domain-background-container {
  position: fixed;
}

.domain-hotspots {
  width: 100vw;
  height: 100vh;
  position: fixed; 
  background: blue; 
}

#hotspot {
  width: 2em;
  height: 2em;
  background: green; 
}

#hotspot:hover .bubble {
  background: red; 
}

#page-nav {
  width: 100%;
  text-align: center;
  background: yellow; 
  position: relative;
}

#page-nav:hover p {
  color: red; 
}
  <div id="domain-background-container">
    <div class="domain-hotspots">
      <div id="hotspot">
        <p class="bubble">I'm red on hover</p>
      </div>
    </div>
  </div>

  <div id="page-nav" >
    <p>I'm red on hover</p>
  </div>

Upvotes: 5

Related Questions