tags:

views:

74

answers:

2
+3  Q: 

CSS layout mystery

Among the many two (or three) column layout techniques I sometimes use the following one:

<div class="left1">
  <div class="left2">
    left main content
  </div>
</div>
<div class="right1">
  <div class="right2">
    right sidebar
  </div>
</div>

together with:

.variant1 .left1 {
  float: left;
  margin-right: -200px;
  width: 100%;
}
.variant1 .left1 .left2 {
  margin-right: 200px;
}
.variant1 .right1 {
  float: right;
  width: 200px;
}

This works in all major browsers. But for some very strange reason exactly the same technique but reversed doesn't work:

.variant2 .left1 {
  float: left;
  width: 200px;
}
.variant2 .right1 {
  float: right;
  margin-left: -200px;
  width: 100%;
}
.variant2 .right1 .right2 {
  margin-left: 200px;
}

In the second variant all text in the sidebar cannot be selected and all links cannot be clicked. This is at least true for Firefox and Chrome. In IE7 the links can at least be clicked and Opera seems completely fine.

Does anyone know the reason for this strange behaviour? Is it a browser bug?

Please note: I am not looking for a working two column CSS layout technique, I know there are loads of them. And I don't necessarily need this technique to work. I only like to understand the reason why the second variant behaves like it does.

Here is a link to a small test page which should illustrate the problem: http://selfthinker.org/stuff/css_layout_mystery.html

A: 
.variant2 .right1 {
   margin-left:-200px;
}

Fix'd... in FF.

The float: right; made it float above the lower DOM layer, making it inaccessible to clicks, and the width:100% forced it to take up the parent's width (page), pushing it up.

It's not a browser bug, and I'm pretty sure this is documented, but it may have something to do with the order elements are rendered in (which could be browser-specific).

David Titarenco
Hmm, okay this fixes the problem when it is compressed like in the example on that test page. But this isn't a solution for a usable layout technique as no float clearing could be done inside the main content. (But it can in the first variant.)But your explanation of why it doesn't work sounds very plausible. Thanks for that.
+1  A: 

It's a basic layering issue.Let's parse the CSS spec on visual formatting to find out why.

The order in which the rendering tree is painted onto the canvas is described in terms of stacking contexts.

Okay, let's see what "stacking context" we're in. For that, we'll need to know when a new stacking context is created.

[A z-index value of an]...integer is the stack level of the generated box in the current stacking context. The box also establishes a local stacking context in which its stack level is '0'.

Well, we don't have any z-index values - so they're all auto.

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'.

Nope, no positioned elements either. Looks like we're all in the "root stacking context".

Boxes with the same stack level in a stacking context are stacked back-to-front according to document tree order.

That certainly explains why .right1 paints over .left1 - it's after it in source order. (Note that you'd see the paint-over issue better if you removed the margin-left: 200px from .right2).

So, now that we know the problem (and that it's according to spec) - how do we fix it? Easiest thing to do would be just make the z-index of .left1 to be higher than .right1. Since they're in the same stacking context, the higher z-index will override the source order:

.variant2 .left1 { position: relative; z-index: 1; }

Or, if we keep reading the spec - we'll notice that:

Each stacking context consists of the following stacking levels (from back to front):

  • the background and borders of the element forming the stacking context.
  • the stacking contexts of descendants with negative stack levels.
  • stacking level containing in-flow non-inline-level non-positioned descendants.
  • stacking level for non-positioned floats and their contents.
  • a stacking level for in-flow inline-level non-positioned descendants.
  • a stacking level for positioned descendants with 'z-index: auto', and any descendant stacking contexts with 'z-index: 0'.
  • the stacking contexts of descendants with positive stack levels.

which means we can actually just do:

.variant2 .left1 { position: relative; }

which will give .left1 a "stacking level" of 6 - which will override .right1's stacking level of 4.

Mark Brackett
Wow, that's what I call an elaborate and precise answer. Thanks!Of course, I tried z-index before, but what I didn't understand was this:> "Other stacking contexts are generated by any positioned element (including relatively positioned elements) having a computed value of 'z-index' other than 'auto'."Z-index didn't work because my boxes were not positioned. Then `.variant2 .left1 { position: relative; }` will not work as soon as `.right1` is relatively (or other) positioned as well. But then a set z-index will finally work and solve the issue.Okay, I understand now. Thanks! :)
@selfthinker - you're right that just positioning `.left1` is fragile. I just wanted to get that bit about stacking levels in there, as it often trips up folks when it acts as an "implicit" z-index.
Mark Brackett