Introduction
When creating webpages, we typically think of them as being arranged in two directions: vertically (Y-axis) and horizontally (X-axis). This is because the default layout mode that is used by HTML and CSS is the flow layout mode. In this mode, elements are stacked either side by side (inline) or on top of each other (block-level), based on the order they appear in the HTML.
When we want to change this default behaviour, we set display properties to flex
or grid
and apply some other properties that determines how it should behave or use the float
property.
However, there are instances where we want to overlap or stack elements on top of each other and this is where the CSS position
and z-index
properties amongst others come in.
Position
The position
property is used to determine the positioning type of an element. It is commonly used with other properties like top
, right
, bottom
and left
to determine the location of the elements that it is applied on. With the default value of static
, elements are positioned according to the normal flow of the document.
Our interest here is the absolute
positioning type. This property is used to position an element relative to its closest positioned ancestor element. If there is no positioned ancestor, the document body is used instead. This value allows us to overlap elements on top of each other.
z-index
The z-index
property transforms our conceptualization of a webpage from a 2-Dimensional model to a 3-Dimensional one. This property helps us determine the stacking order of elements. The higher the value of z-index
, the closer the element is to the front — that is the user's viewport, the lower the value, the further the element is.
As a result of this, elements with a higher z-index
value will be on top of elements with a lower z-index
. The default value of z-index
is 0
and it is applied on every element.
You can have a value of
auto
which is the default value and it is calculated based on the stacking order of the elements.number
which is a positive or negative whole number.
Visual Representation
Before we go on, below is a visual representation of the z-index
property. Each transparent blue layer represents a different z-index
level. You can play around with the controls to see how the elements are stacked.
<!DOCTYPE html> <html> <head> <title>z-index visualization</title> <meta charset="UTF-8" /> <link rel="stylesheet" href="/styles.css" /> </head> <body> <div class="controls"> <div> <label for="rotateYSlider">Rotate Y:</label> <input type="range" min="-360" max="360" value="60" step="1" id="rotateYSlider" /> </div> <div> <label for="rotateXSlider">Rotate X:</label> <input type="range" min="-360" max="360" value="-20" step="1" id="rotateXSlider" /> </div> </div> <div class="scene"> <div class="cube"> <!-- Base layer --> <div class="layer layer-1">Base Layer (z-index: auto)</div> <!-- Parent stacking context --> <div class="layer layer-2"> <div class="parent"> Parent Context <div class="element" style="top: 10px; left: 10px;">Child</div> </div> </div> <!-- Higher z-index layer --> <div class="layer layer-3"> z-index: 1 <div class="element" style="top: 30px; left: 30px; background: rgba(0, 0, 255, 0.8);" > Sibling </div> </div> <!-- Top layer --> <div class="layer layer-4">z-index: 2</div> </div> </div> </body> <script src="/script.js"></script> </html>
In the example above, we have four layers:
- A base layer which is the lowest layer and is positioned at the bottom of the stack.
- A parent stacking context which is the second layer and is positioned on top of the base layer.
- A higher z-index layer which is the third layer and is positioned on top of the parent stacking context.
- A top layer which is the fourth layer and is positioned at the top of the stacking context.
It is very important for you to note that in this example, we didn't use the
z-index
property to position the layers. Instead, we used thetransform
property.
Stacking Context
It is evident that every element in a webpage is positioned either on top of another element or underneath another element. This groups elements into what we refer to as a Stacking Context.
A new Stacking Context is formed:
- on the root element (
<html>
) or - when an element has any of these properties:
position
set torelative
orabsolute
with az-index
value other thanauto
.position
set tofixed
orsticky
.opacity
less than 1.transform
,filter
,perspective
, orclip-path
properties.display
set toflex
orgrid
with az-index
value other thanauto
.contain: layout
orcontain: paint
.will-change
properties.isolation: isolate
.mix-blend-mode
set to anything other than normal.
This implies that every element in a webpage is a part of a default stacking context. Once a new stacking context is created, the elements within it are painted independently of elements outside the context. This means that:
- The
z-index
of elements within a stacking context does not affect elements in sibling or parent stacking contexts. - Only the stacking context as a whole competes with other elements in the parent stacking context.
This isolation allows us to
- carefully control rendering logic when elements overlap each other
- update the stacking order without affecting the rest of the layout.
Earlier, I made a note that the visual representation of how elements were stacked in the z-axis wasn't done with
z-index
but rather withtransform
. This was because using thez-index
in conjunction withposition: absolute
would have created a new stacking context.
Stacking Order
The Stacking Order determines the order in which elements are painted on the screen. This is a simplified version of the Painting Order algorithm:
- Start with the background and border of the stacking context itself.
- Move through floats, in-flow block-level and inline-level content in DOM order.
- Handle child stacking contexts in order:
- Negative z-index.
- Stack level 0 (auto or explicitly 0).
- Positive z-index (from smallest to largest).
Things to note
- Stacking contexts are self-contained: Child elements can never be rendered in front of their parent.
- Each stacking context is independent:
z-index
values only compete within the same stacking context. - Once a new stacking context is created, its descendants' z-indices are only compared within that context.
- Stacking contexts are hierarchical: Elements work their way up the tree to find the nearest stacking context.
Stacking Context in action
Now that we understand the basics of stacking context and how it affects the rendering of elements, let's see how it works in action.
Example 1
<div class="parent">
<div class="child-1">Child 1</div>
<div class="child-2">
<div class="grandchild">Grandchild</div>
</div>
</div>
.parent {
position: relative;
z-index: 1;
}
.child-1 {
position: absolute;
z-index: 2;
}
.child-2 {
position: absolute;
z-index: auto; /* control */
}
.grandchild {
position: absolute;
z-index: 999;
}
In the example above, how many stacking contexts do you think are there apart from the root stacking context? And which element shows up on top?
There are three stacking contexts:
- The stacking context on
.parent
. - The stacking context on
.child-1
. - The stacking context on
.grandchild
.
The main thing to note here is that .child-2
doesn't create a new stacking context and because of this, the element that shows up on top is .grandchild
as it has the highest z-index
value in the .parent
stacking context.
But what happens when you change the z-index
of .child-2
to 1
?
This creates a new stacking context and the element that shows up on top is .child-1
as it has the highest z-index
value in the .parent
stacking context. Remember that when a stacking context is created, its descendants' z-indices are only compared within that context.
Example 2
<div>
<div>
<div class="red">Red</div>
</div>
</div>
<div>
<div class="green">Green</div>
</div>
<div>
<div class="blue">Blue</div>
</div>
</div>
.red,
.green,
.blue {
position: absolute;
width: 100px;
color: white;
line-height: 100px;
text-align: center;
}
.red {
z-index: 1;
top: 20px;
left: 20px;
background: red;
}
.green {
top: 60px;
left: 60px;
background: green;
}
.blue {
top: 100px;
left: 100px;
background: blue;
}
This example is a bit trickier. How many stacking contexts are there apart from the root stacking context? And which element shows up on top?
There is only one stacking context:
- The stacking context on
.red
. This is because.red
is the only positioned element with az-index
value while others have an implictz-index: auto
.
This is the same element that is on top. This is because it has the highest z-index
value in the root stacking context.
<!DOCTYPE html> <html> <head> <link rel="stylesheet" href="/styles.css" /> </head> <body> <div class="container"> <div> <div class="controls"> <div> <label for="rotateYSlider">Rotate Y:</label> <input type="range" min="-360" max="360" value="60" step="1" id="rotateYSlider" /> </div> <div> <label for="rotateXSlider">Rotate X:</label> <input type="range" min="-360" max="360" value="-20" step="1" id="rotateXSlider" /> </div> </div> <div class="scene"> <div class="cube"> <div class="context-boundary root-context"> <span class="context-label">Root Stacking Context (html)</span> <div class="context-boundary red-context"> <span class="context-label" >Red's Stacking Context (position + z-index)</span > <div class="element red">Red z:1</div> </div> <div class="element green">Green</div> <div class="element blue">Blue</div> </div> </div> </div> </div> </div> </body> <script src="/script.js"></script> </html>
What happens when you add filter: grayscale(0.8)
to the parent of .red
?
The element that shows up on top is .blue
. This is because adding a filter to the parent of .red
creates a new stacking context and its z-index
gets compared within this context.
<!DOCTYPE html> <html> <head> <link rel="stylesheet" href="/styles.css" /> </head> <body> <div class="container"> <div> <div class="controls"> <div> <label for="rotateYSlider">Rotate Y:</label> <input type="range" min="-360" max="360" value="60" step="1" id="rotateYSlider" /> </div> <div> <label for="rotateXSlider">Rotate X:</label> <input type="range" min="-360" max="360" value="-20" step="1" id="rotateXSlider" /> </div> </div> <div class="scene"> <div class="cube"> <div class="context-boundary root-context"> <span class="context-label">Root Stacking Context (html)</span> <div class="context-boundary filter-context"> <span class="context-label">Red's Parent (filter)</span> <div class="element red">Red z:1</div> </div> <div class="element blue">Blue</div> <div class="element green">Green</div> </div> </div> </div> <div class="structure"> DOM Structure: <pre> root stacking context ├── div (filter: grayscale) │ └── red (z-index: 1) ├── green └── blue</pre > </div> </div> </div> <script src="/script.js"></script> </body> </html>
Best Practices
As you have seen, it is very easy to create numerous stacking contexts and spend a lot of time trying to debug why z-index
or absolute positioning isn't working as expected and instances where setting a high value doesn't change anything.
The following are some best practices you can try out to have a more predictable stacking context setup.
Use z-index
sparingly
You can try creating a scaling system for your z-index
values. This way you can reduce your usage of setting abritarily high values.
:root {
--z-negative: -1;
--z-normal: 0;
--z-dropdown: 100;
--z-sticky: 200;
--z-modal: 300;
--z-popover: 400;
--z-tooltip: 500;
}
Minimize New Contexts
Review your CSS styling to make sure that you create stacking contexts intentionally and not by accident.
.accidental-context {
opacity: 0.99; /* Creates a stacking context! */
transform: translateZ(0); /* Creates a stacking context! */
will-change: transform; /* Creates a stacking context! */
}
Use Modern CSS properties
The isolation
property is particularly useful when you want to create a new stacking context without modifying other properties:
.needs-isolation {
isolation: isolate; /* Creates a stacking context! */
}
When using features like container queries, be aware that using some properties might cause a new stacking context to be created.
.container {
container-type: size: /* Creates a stacking context! */
}
Summary
Understanding stacking contexts helps you provide solutions to layout issues especiallly when it comes to overlapping elements. Here are the key takeaways:
- Every element exists within a stacking context, with the root element creating the initial one.
- Multiple CSS properties can create new stacking contexts, not just
position
andz-index
. - Stacking contexts are hierarchical - children can't escape their parent's stacking context.
- Modern CSS provides cleaner ways to manage stacking contexts through properties like
isolation
.
Remember: when in doubt about why elements aren't stacking as expected, check if you've accidentally created new stacking contexts, and use your browser's DevTools to inspect the computed styles and stacking order. Microsoft Edge has a 3D view that can help visualize the stacking order.