woensdag 14 juli 2010

Stacking solves stuff

In my thesis I investigate the possibility of developing a tool that visualizes any web page as a 3D model - It is called DomScape. If you don't know what I am talking about, here's a video of the first mock up.


While writing a new post about the position property and how and when it causes overlap, I ran into a very important piece of CSS specification that I had never heard of. 'Stacking context' defines in what order elements should be rendered above one another. The stacking context is intertwined with other new concepts that I introduce in this post: stacking order, source order, z-index and stacking layers.

In this post I will explain how stacking context works and how it deals with overlap. I will then take this concept and put it into perspective of DomScape. I finish with a pseudo-algorithm that calculates z-values using stacking contexts.

What is stacking?
Stacking is used in CSS to determine what happens when elements overlap, i.e. occupy the same x,y-space. If we look at a group of overlapping elements, one of the elements is rendered on top and other elements are rendered below. We could say that there is a certain stacking order - the element that is rendered on top is the highest in this order and the element that is rendered at the bottom has the lowest position in the stacking order.

In my last post I looked at overlapping elements in normal flow. When elements overlap in normal flow, the order in the source code determines the stacking order. The element that occurs later in the code is rendered on top.

This figure demonstrates that source order determines the stacking order in overlapping elements in normal flow. Elements that occur later in the code are rendered on top of elements that occur earlier. 

As it turns out, stacking in normal flow is just a special case of stacking in general. CSS properties, such as float or position, also determine an element's position in the stacking order. The z-index property which can be set for elements that are not in normal flow (i.e. have a position other than static) can be used to determine the stacking order with greater control. If two elements overlap, the element with the highest z-index will be rendered on top.

Stacking layers

The CSS 2.1 specification defines seven layers that determine the stacking order of a group of elements that belong together. Elements are grouped because they all descent from the same stacking context. By default the stacking context is established by the highest element in the HTML hierarchy, the body element. As we will see later, any element can be made to establish a stacking context for its descendants.

Starting from back to front, there are seven layers. Every element belongs to either of these layers.
  1. The background and borders of the element that establishes the stacking context
  2. Positioned descendants with negative z-index
  3. Block-level descendants in the normal flow
  4. Floating descendants and their contents
  5. Inline-level descendants in the normal flow
  6. Positioned descendants with z-index set to auto or 0
  7. Positioned descendants with z-index greater than 0
This list can be quite overwhelming, so let's take a good look at it to see what it all means. 

Breaking it down
The third and fifth layer contain all the normally flowed elements. All elements in the negative margin example from my previous post exist within that layer. So, if two elements overlap and are in the same layer, then the source order determines the stacking order of those elements.

This example from my previous post demonstrates that when all elements are within the same layer, source order determines stacking order.

Because in the last example the text of the elements never overlap, I never noticed that inline-level elements always render on top of block-level elements. Although both types of element are part of the normal flow, block-level elements are rendered in the third layer and inline-level elements in the fifth layer.

The following example illustrates the two different ways of stacking (layer order vs source order). On the right an element is repositioned using negative margin. This causes overlap within two different layers and between those two layers as well. On the third layer the block-level elements overlap and on the fifth layer the two inline elements overlap.

Three times overlap: once between two layers and twice within layers.

Between the third and the fifth layer, the fourth layer is reserved for floated elements and their contents. If an element were floated in such a way that it would overlap with a displaced, but normally flowed, element, the floated element is always rendered on top, because it is in a higher layer. The following figure demonstrates that the elements are rendered differently when floating is involved.

Example of float element rendered over a block-level element. 

It is clear that the inline-level element wraps to the next line in order to avoid overlapping the floated element. However, if the elements are forced to overlap, the inline text will render on top of the floated element. This is a somewhat unexpected - but correct - behavior, because the floated element is rendered between a parent and child element, i.e. the yellow block-level element and the inline-level text element respectively.

Example of a float element rendered on top of a block-level element, but under the inline-level that is contained by that block-level element.


It is not necessarily the case that inline-level elements always overlap floated elements. If we take a good look at the layer 4 description, it explicitly mentions that not only floated elements are in that layer, the contents of the floated element are as well. Thus, when we let two floated elements overlap, both elements and their contents are in the same layer. At that point, source order determines stack order, as we have seen before.

When two floated elements overlap, source order determines which element is rendered on top.

It is important to note that 'contents' does not mean any kind of element. For example, if we declare a 'contents' element to be in layer six, that element is taken out of the fourth layer and rendered in the sixth layer, on top of the floated elements and (the rest of) the contents.

When two floated elements overlap, they and their contents are rendered in the fourth layer. If an element (in this case the inline-level 'I am floating' text) is made to render in a higher layer, it overlaps the floated elements.

Layer two, six and seven are reserved for elements that are not in normal flow, i.e. elements that have position set to either relative, absolute or fixed. For these elements the stacking order is determined by the z-index of each element. It is important to note here that most browsers reverse the first and second rule. This means that positioned elements with negative z-index are (incorrectly) rendered behind the stacking context's borders and background. Many web developers (unknowingly) abuse this bug to hide elements - in stead of using the display property. When a browser correctly implements the CSS specification, a element with a negative z-index is only completely hidden when an element with a higher z-index completely overlaps it.

Nested contexts
Layer one forms the absolute bottom of the stacking order. By default, the body element establishes the stacking context of the rest of the elements. However, there is often more than one stacking context in one web page. When an element has a z-index other than auto or 0, that element automatically establishes a local stacking context for all of its descendants. Only positioned elements can form new stacking contexts because, as we saw earlier, the z-index property can only be applied to positioned elements. In short, all elements in layer two and seven form new stacking contexts for their descendants.

Every element belongs to a stacking context. Most elements belong only to one stacking context, but when an element also establishes a new stacking context, to what stacking context does the element belong? In the original stacking context the element is either in the second or seventh layer, but it the self-established stacking context it (solely) occupies layer one.

Stacking order
Every stacking context has its own stacking order, which is relative to the element that established the stacking context. One interesting consequence of this is that an element can never 'escape' its stacking context: it is confined to the stacking level of the element that established the stacking context.

For example, imagine two elements, A and B, that are both absolutely positioned, overlap and are within the same stacking context. A is rendered on top B, because A has a higher z-index than B. As both elements have a z-index, they establish stacking contexts for their descendants. B contains a element, B', that is absolutely positioned with a z-index higher than 0. Because B' is confined to the stacking level of its stacking context (B), and A is rendered on top of B, B' cannot render on top of A - no matter what z-index we assign to B'.


A is rendered on top of B, because A has a higher z-index. A and B both establish new stacking contexts, thus B' is rendered inside its stacking context and will never overlap A or A' - no matter what z-index we assign to B'.

Visualizing stacking order
Stacking order determines which elements render on top. It is thus quite obvious that the stacking order is an important factor in determining the z-value for each element. In order to assign z-values to the elements in line with the stacking order, we need to know the following about each element:
  • x,y-space it occupies
  • its stacking context
  • its stacking layer
  • its z-index
  • its position in the source code
With this information we can sort the elements in such a way that all have an unique z-value and are in the same order as the stacking order. The following steps are executed:
  1. Take all elements that are in the first stacking context
  2. Sort those elements by the stacking layer, starting with the first layer
  3. If the layer is two, six or seven, sort elements that belong to the same layer by their z-index, starting with the lowest z-index
  4. Sort elements that have the same z-index by their position in the source code, starting with the earliest occurring element (even if it the z-index is not defined).
  5. If an element establishes a new stacking context, do these four steps again for that stacking context.
In the following example, the gray background establishes the stacking context and therefore has the lowest z-value. A and B are both in the sixth layer. A has a higher z-index than B, thus A has a higher z-value. Because B establishes a new stacking context for B', A must also have a higher z-value than B'. A' has a higher z-value than A, because A establishes the new context for A'.


Now that we know the order in which the elements are, it is easy to calculate the z-values for the elements: background: 0, A: 3, A': 4, B: 1, B': 2. This gives every element an unique position in x,y,z-space. The elements are also in the right order, which is necessary to render the web page correctly and to achieve a smooth transition between the 2D and 3D view. However, every z-value only contains one element. This is not very usable, because elements are probably too far apart to properly understand the context of the elements. We thus have to collapse the 3D model so that it becomes more compact and usable.

We can achieve this by allowing elements to have the same z-value as long as they don't occupy the same x,y-space. By doing this we allow elements from different layers and even different contexts to have the same z-value. If we take a look at the same example, we arrive at different - and not very surprisingly, lower - z-values: background: 0, A: 1, A': 2, B: 1, B': 2.

Z-algorithm
We can try to formalize the z-value calculation. The following pseudo-algorithm calculates the z-values for all elements according to the stacking context. I took the algorithm from my last post and adjusted it to use the stacking context to calculate the z-values.

e, f: element

E: collection of all elements  
C: collection of all elements in the initial stacking context
F: collection of elements with calculated z

source_sort E
z-index_sort E
layer_sort E
context_sort E

assign_z_to C of E
 
function assign_z_to: context C
  for every element e in C:
    z of e = 0 
     
    for every element f in F:
      if e overlaps with f
        z of e = 1 + z of f

    add e to F
    if e establishes context C'
      assign_z_to C'     

Basically, the difference between the original algorithm and the new one are in two things.

First, the elements in the original algorithm were sorted only on source order. In this algorithm all elements are sorted on the stacking context they belong to. All elements that belong to the same context are then sorted by their stacking layer. If the element is in layer two or seven, the elements are sorted by z-index as well. Last but not least, if elements are in the same stacking context, in the same stacking layer and have the same z-index (or no z-index), sort those elements by their order in the source code.

Second, when an element establishes a new stacking context, the z-values for this context are calculated first, before continuing with the rest of the current stacking context. Because the elements in the new stacking context could be overlapped by later elements, it is important to know the z-values of the elements in the lower layers first.

Conclusion
The discovery of CSS stacking has been a real eye opener. I have been making web sites for the past ten years and I had never come across it before. While writing this post I realized that stacking contexts are the answer to my first research question: Is the DomScape visualization possible? I asked this question, because I had no idea how CSS determines how elements should be rendered when they overlap. I figured I had to go through every CSS positioning and determine myself how the elements interact. The fact that CSS has a very straightforward approach is definitely helpful: it helps me answer the first question with much more confidence and accuracy and it will buy me precious time that I can spend on the other two questions: 

  2. What is needed to implement DomScape?
  3. What are DomScape's usability challenges?
    More reading
    CSS Discuss: Overlapping and z-index
    Sitepoint: Stacking contexts 
    Sitepoint: z-index
    W3C: Elaborate description of Stacking Contexts