Self-importance. Entitlement. Snobbery.

IE7 lessons learned: the z-index bug

Jan 11 2008

The IE z-index bug had been around since IE4 or 5, and was finally fixed in IE8. It’s still an issue in IE7, however, and chances are that you, the web developer, still have to support it. You should read Aleksandar Vacić’s in-depth characterization of the problem to really understand what’s happening, but what it boils down to is this: the CSS 2.1 spec says that a positioned element with any integer z-index value (i.e. not auto) should create its own zero-based stacking context, and use the integer value specified to decide its place in its parent stacking context. In other words, if the positioned element has a z-index of auto, its stacking context is inherited from its parent. Internet Explorer, however, creates a new stacking context for elements with any z-index value, including auto, which wreaks all kinds of havoc and generally causes mayhem in your previously neat and orderly layouts.

So what does this mean for you? Well, if you, like many people, use any variation of ALA’s Suckerfish dropdown menus on your site, you’ll notice that any positioned element, with any z-index, that is located lower down in the code than the menus will appear above the menus—no matter what z-index the menu is given—exactly the opposite of what you want to happen. If you use IE6 or below, you’ll also see form elements like the infamous IE select box appear above the menu.

Now you know what the problem is, let’s talk about how to work around it. If the problem you’re having is with form elements bleeding through, one of the solutions described here, specifically Hedger Wang’s solution (placing your menu above an iframe with a z-index of -1), should fix you right up. Unless, of course, you have subsequent positioned and z-indexed elements in your code. Then it becomes a lot harder.

And, surprise!, that’s exactly the problem I had. I had a bunch of relatively positioned UI widgets with absolutely positioned children, each with its own flyout menu of actions:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
a
{
  color: black;
  text-decoration: none;
}

#main
{
  margin: 50px auto;
  padding: 10px;
  width: 350px;
}

.block
{
  position: relative;
  margin: 0px 0px 10px 0px;
  width: 100%;
  height: 25px;
  background-color: #e0d0d0;
}

.badge
{
  position: absolute;
  top: 0px;
  left: 0px;
  width: 21px;
  height: 21px;
  background-color: white;
  border: 2px solid #c0b0b0;
  color: #c0b0b0;
  line-height: 21px;
  text-align: center;
  font-size: 13px;
  font-weight: bold;
}

.info
{
  position: absolute;
  top: 0px;
  right: 0px;
  left: 25px;
  bottom: 5px;
  padding: 0px 5px;
  background-color: #f0e0e0;
  border: 1px solid #f0d0d0;
  line-height: 18px;
}

a.menu-activator-arrow
{
  display: block;
  position: absolute;
  top: 0px;
  right: 0px;
  width: 20px;
  height: 18px;
  background-color: white;
  border-left: 1px solid #e0c0c0;
  line-height: 18px;
  text-align: center;
  color: #c0a0a0;
  font-weight: bold;
  text-decoration: none;
}

a.menu-activator-arrow:hover
{
  background-color: #a07070;
  color: white;
}

.menu
{
  position: absolute;
  top: 19px;
  right: 0px;
  z-index: 10;
  background-color: #f0f0f0;
  border: 1px solid #a0a0a0;
}

.menu ul
{
  margin: 0px;
  padding: 0px;
  list-style: none;
}

.menu a
{
  display: block;
  padding: 2px 5px;
}

.menu a:hover
{
  background-color: #808080;
  color: white;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<div id="main">
  <div class="block">
    <div class="badge">Ad</div>
    <div class="info">
      some info
      <a class="menu-activator-arrow" href="javascript://">v</a>
      <div class="menu" style="display: none;">
        <ul>
          <li><a href="javascript://">choice one</a></li>
          <li><a href="javascript://">choice two</a></li>
          <li><a href="javascript://">choice three</a></li>
        </ul>
      </div> <!-- /menu -->
    </div> <!-- /info -->
  </div> <!-- /block -->

  <div class="block">
    <div class="badge">Ad</div>
    <div class="info">
      some info
      <a class="menu-activator-arrow" href="javascript://">v</a>
      <div class="menu" style="display: none;">
        <ul>
          <li><a href="javascript://">choice one</a></li>
          <li><a href="javascript://">choice two</a></li>
          <li><a href="javascript://">choice three</a></li>
        </ul>
      </div> <!-- /menu -->
    </div> <!-- /info -->
  </div> <!-- /block -->

  <div class="block">
    <div class="badge">Ad</div>
    <div class="info">
      some info
      <a class="menu-activator-arrow" href="javascript://">v</a>
      <div class="menu" style="display: none;">
        <ul>
          <li><a href="javascript://">choice one</a></li>
          <li><a href="javascript://">choice two</a></li>
          <li><a href="javascript://">choice three</a></li>
        </ul>
      </div> <!-- /menu -->
    </div> <!-- /info -->
  </div> <!-- /block -->
</div> <!-- /main -->
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// When the DOM is ready,
$(function() {
  /*
  ** For each link with class "menu-activator-arrow" inside
  ** an element with class "block",
  */
  $(".block a.menu-activator-arrow").each(function() {
    // When that link is clicked,
    var $link = $(this);
    $link.click(function() {
      // Show or hide the element with class menu that follows it.
      $link.next(".menu").toggle();
    });
  });
});

And it looked like this in IE7:

How it looks in IE7

The upshot of Vacić’s article is that the relatively positioned parent of your absolutely positioned menu should be given a specific integer z-index that is higher than those of subsequent positioned elements on the page in order to ensure that your popup menu appears above all other elements. However, that requires you to know precisely how many relatively positioned elements are in the page, and to manually assign a z-index to each. But in the context of my application, these widgets were dynamically generated and I didn’t know how many I would have. So how did I deal with it? The brute force way. After all the widgets had been loaded, I searched for all divs with class block, and iterated through the list, assigning subsequently smaller z-indexes to each, thereby enabling the menu belonging to the topmost widget to appear above all subsequent widgets:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function isIE()
{
  if(navigator.userAgent.match(/MSIE \d\.\d+/))
  {
    return true;
  }
  return false;
}

function zIndexWorkaround()
{
  if(isIE())
  {
    var zi = 1000;
    $(".block").each(
      function() {
        $(this).css("z-index", zi--);
      }
    );
  }
}

But that’s a bit ugly. Vacić himself suggested a more elegant solution: just dynamically increase the z-index of any positioned ancestor element when your mouse is over it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function zIndexWorkaround()
{
  // If the browser is IE,
  if(isIE())
  {
    /*
    ** For each div with class menu (i.e.,
    ** the thing we want to be on top),
    */
    $("div.menu").each(function() {
      // For each of its ancestors,
      $(this).parents().each(function() {
        var $ancestor = $(this);
        var pos = $ancestor.css("position");

        // If it's positioned,
        if(pos == "relative" ||
           pos == "absolute" ||
           pos == "fixed")
        {
          /*
          ** Add the "on-top" class name when the
          ** mouse is hovering over it,
          */
          $ancestor.hover(function() {
            $ancestor.toggleClass("on-top");
          });
        }
      }
    }
  }
}

Where on-top is defined thusly:

1
2
3
4
.on-top
{
  z-index: 10000;
}

And then you’d just call zIndexWorkaround() from inside your domready call.

Hope this is of some help.