Introduction
Hi, I’m (flitter)[https://flitter.dev] maintainer and recently I provide z-index widget.
This article explores the workings of the z-index property and how to implement it within SVGs.
Intended Audience
Those struggling with z-index not applying as expected.
Anyone unfamiliar with the concept of stacking context.
Individuals looking to implement z-index directly in SVG.
People interested in adding z-index behavior to charts or diagrams.
Keywords
What is Z-Index?
The z-index is a CSS property that adjusts the vertical stacking order of elements. Its default value is auto, equivalent to z-index: 0. However, z-index: 0 creates a new stacking context, altering the precedence for child elements differently from z-index: auto. A stacking context will be explained further below.
Typical Z Order
In the absence of a specified z-index, the vertical order of stack elements matches the order in which DOM nodes are rendered. This ordering is consistent with a Depth-First Search (DFS), where elements rendered later take precedence over those rendered earlier. For example, a pink box drawn over an orange box is higher in the stack..
Adding Z Elements
Assigning a z-index of 9999 to a purple circle places it at the top, as shown in the illustration. Despite the pink box being the last reach in a depth-first search, the z-index rearrangement prioritizes the purple circle to be drawn last.
What is a Stacking Context?
Stacking context, along with an element’s z-index, determines its vertical stack priority. Simply put, think of a stacking context as an array of a parent’s z-index values. Even if a child element has a high z-index, it can still be placed lower in priority due to its stacking context.
As shown in the illustration above, despite the purple circle having a z-index of 9999, it is obscured by the pink box below because it belongs to a new stacking context created by its parent’s z-index of 0.
Situations Creating Stacking Context
Stacking context can form not only when a parent element’s z-index is specified but also under the following conditions:
Root element of the document (<html>).
Element with a position value absolute or relative and z-index value other than auto.
Element with a position value fixed or sticky (sticky for all mobile browsers, but not older desktop browsers).
Element with a container-type value size or inline-size set, intended for container queries.
Element that is a child of a flex container, with z-index value other than auto.
Element that is a child of a grid container, with z-index value other than auto.
Element with an opacity value less than 1 (See the specification for opacity).
Element with a mix-blend-mode value other than normal.
Element with any of the following properties with value other than none:
transform
filter
backdrop-filter
perspective
clip-path
mask / mask-image / mask-border
Element with an isolation value isolate.
Element with a will-change value specifying any property that would create a stacking context on non-initial value (see this post).
Element with a contain value of layout, or paint, or a composite value that includes either of them (i.e. contain: strict, contain: content).
Element placed into the top layer and its corresponding ::backdrop. Examples include fullscreen and popover elements.
Stacking Context Operational Rules
Let’s delve into how stacking contexts are formed and influence the order in which nodes are drawn, explained step by step. For those interested in directly jumping to the implementation logic of z-index, please proceed to the next section titled “Z-Index Implementation Logic.”
The illustration above represents a schematic of the DOM structure. The numbers on each node denote the order they are visited in a preorder traversal. Nodes 2 and 4 possess distinct stacking contexts. Node 3 inherits the stacking context from node 2.
Comparing nodes 2 and 4, even though node 4 has the same z-index, its higher node number grants it priority in the stacking order. Since node 3 inherits the stacking context from node 2, no matter how high its z-index, it will be ranked lower in the stacking order compared to node 4.
Example: Complicated Stacking Context
In the illustration, nodes 3 and 4 are situated below node 2, and node 7 is located beneath node 6. Notably, there exists a stacking context with a z-index of -1, as seen with node 6, and there are nodes, like node 5, that do not belong to any stacking context. Nodes without a separate stacking context are considered to have a z-index of 0.
The nodes can be divided according to the drawing order as shown above. Node 1 takes precedence over nodes 6 and 7 in the vertical layer, and node 5 is superior to nodes 2, 3, and 4 in the vertical layer. Since nodes 3 and 4 are both below node 2, node 3 with the higher z-index takes precedence over node 4 in the vertical layer.
Therefore, the drawing order is as follows:ㅋ
Z-Index Implementation Logic
Implementing z-index equates to creating a logic for arranging DOM drawing order, considering the stacking contexts. A stacking context can be identified through a pre-order traversal of the DOM tree. First, let’s define a Stacking Context.
zIndex: number //stacking context를 형성한 dom의 z-index
domOrder: number //stacking context를 형성한 dom의 전위순회 순서
}
type CollectedDom = {
contexts: StackingContext[] //dom이 물려받은 stacking context
domOrder: number //dom의 전위순회 순서
dom: HTMLELEMENT
}
A StackingContext is formed by a DOM element that has a z-index. It collects the zIndex and domOrder of the formed DOM into the stacking context. A child inherits the StackingContext from its parent. If the child has a new z-index, it adds a new context to the Stacking context inherited from the parent. Then, the CollectedDom information corresponding to node 7 would be as follows:
contexts: [{zIndex: –1, domOrder: 6}] //6번 노드로부터 물려받음
domOrder: 7
dom: div태그
}
If you have collected const collectedDoms: CollectedDom[] while traversing the DOM, you can sort them in the correct order by considering the stacking context. There are three points to note:
If they are in the same stacking context, the order is determined by the DOM order.
If there is no stacking context, the z-index is considered to be 0.
In a stacking context, the parent element is drawn first regardless of the z-index of the child elements. (In vertical layer priority, it gives way to its children).
정렬로직
const limit = Math.min(a.contexts.length, b.contexts.length)
/*
stacking context를 순회하며 비교합니다.
*/
for(let i = 0; i < limit; ++i) {
const aContext = a.contexts[i]
const bContext = b.contexts[i]
/*
* stacking context가 서로 다른 경우의 노드는 context의
* zIndex를 먼저 비교하고, 그 다음의 dom 순서를 비교합니다.
*/
if(aContext.zIndex !== bContext.zIndex) {
return aContext.zIndex – bContext.zIndex
} else if(aContext.domOrder – bContext.domOrder) {
return aContext.domOrder – bContext.domOrder
}
}
/*
* 이 아래부터는 a,b는 동일한 stacking context 있음을 의미한다.
* a와 b의 관계는 아래 둘 중 하나이다.
* 1. 부모 – 자식
* 2. 형제
*/
// 1. 부모-자식인 경우
if(limit > 0) {
const lastContext = a.contexts[limit–1] // 값이 같기 때문에 a,b 둘 중 어느것이여도 상관없다.
//둘 중 하나가 부모임
if(lastContext.domOrder === a.domOrder || lastContext.domOrder === b.domOrder {
return a.domOrder – b.domOrder
}
}
// 2. 형제인 경우
if(a.contexts.length !== b.contexts.length) {
const aContext = a.contexts[limit] || { zIndex:0, domOrder: a.domOrder }
const bContext = b.contexts[limit] || { zIndex:0, domOrder: b.domOrder }
/*
* stacking context가 서로 다른 경우의 노드는 context의
* zIndex를 먼저 비교하고, 그 다음의 dom 순서를 비교합니다.
*/
if(aContext.zIndex !== bContext.zIndex) {
return aContext.zIndex – bContext.zIndex
} else if(aContext.domOrder – bContext.domOrder) {
return aContext.domOrder – bContext.domOrder
}
}
// 그 외는 돔 순서로 결정한다.
return a.domOrder – b.domOrder
}
The above code expresses the logic for sorting by z-index. First, it checks if the stacking contexts are different. If they are different, it prioritizes comparing the zIndex, followed by comparing the DOM order. The length of the stacking contexts can be different. It compares them sequentially based on the shortest context. If the compared stacking contexts are all the same, it proceeds to the next step.
If the compared stacking contexts have the same value, it checks if the two DOMs have a parent-child relationship. If they are in the same stacking context, regardless of the child’s z-index value, the child takes precedence over the parent in the vertical layer.
In the diagram, despite the red circle having a lower z-index value of -1 than its parent, it’s clear that it has a higher priority in the vertical layering. If the parent’s z-index: 0 were to be removed, then the red circle would be positioned under the blue box.
The stacking context for the blue box is [{zIndex: 0, domOrder: 2}], and for the red circle, it is [{zIndex: 0, domOrder: 2}, {zIndex: -1, domOrder: 3}]. Let’s consider adding a purple circle as a child of the blue box.
The stacking context for the purple circle, similar to the blue box, is [{zIndex: 0, domOrder: 2}]. However, because its own domOrder is 3, it does not create a stacking context. As can be seen in the diagram above, the purple circle has a higher priority in the vertical layering than the red circle. Since an unspecified z-index is considered as 0, the red circle with a z-index of -1 is consequently lower in priority.
Implementing in SVG
SVG does not support the z-index property. Instead, elements positioned later in the DOM are given higher priority in the vertical stack. To apply z-index in SVG, one must manually manipulate the order of DOM elements. This is similar in canvas.
Using the flutterjs library, which creates an SVG tree analogous to the DOM tree, I leveraged the Visitor pattern and Stacking Contexts to implement z-index.
Conclusion
This article has provided insights into the z-index property and its practical application within SVG. Writing this piece also allowed me to correct misunderstandings about the stacking context’s operational rules and improve drag behavior in an ERD diagram I’m developing.
References