z-index in CSS: the number that does nothing without position

z-index controls the stacking order of overlapping elements, but has zero effect as long as the element keeps its default, static position.
4 min read
Believemy logo

Two elements overlap on screen, and one of them stubbornly stays behind the other even though it has been given a z-index of 999. This scenario is probably the most copy-pasted CSS bug on programming forums, and it almost always has the same explanation.

z-index controls the stacking order of elements that overlap visually, but it obeys an entry condition that almost nobody knows about the first time they reach for it.


Definition

z-index determines which element appears in front of another when two boxes overlap visually. Its value is an integer, with no unit: the higher it is, the more the element rises above its competitors placed at the same level. A negative value is allowed and pushes the element behind its neighbours, including behind its own parent's background.


The trap: without position, z-index does nothing

Good to know

z-index has strictly no effect on an element whose position is static, its default value. The browser silently ignores the property, with no error message at all, which makes the bug particularly hard to spot for anyone new to CSS.

CSS
/* No effect: position stayed static */
.popup {
  z-index: 999;
}

/* Works: position moved away from the default */
.popup {
  position: relative;
  z-index: 999;
}

Adding position: relative, absolute, fixed, or sticky is enough to unlock z-index, even when no offset like top or left is otherwise needed. Our HTML and CSS course covers this exact habit in its very first layout exercises, so this trap stops catching anyone off guard.


The stacking context

Comparing two z-index values only works if both elements belong to the same stacking context, a technical term that simply describes a group of elements stacked together, like a mini theatre stage with its own backstage. Certain properties, such as an opacity below 1 or a transform, quietly create a new stacking context: a child with a huge z-index can then stay stuck behind an outside element carrying a much lower z-index, simply because it belongs to a different group.


Concrete example: a tooltip stuck behind content

CSS
.card {
  position: relative;
  z-index: 1;
}

.tooltip {
  position: absolute;
  top: 100%;
  z-index: 10;
}

Here, the tooltip appears in front of its parent card without any trouble, because both elements share the same stacking context. The problem only shows up if another card elsewhere on the page also carries a high z-index on its own container: the comparison then happens between the containers, not between the tooltip and the rest of the page.


Best practice: a scale of values instead of random numbers

On a growing project, stacking one z-index: 9999 after another eventually creates an arms race where nobody can tell which element is actually meant to sit in front of which other one. A scale fixed in advance, with wide steps between tiers, avoids that problem entirely.

Use caseSuggested tier
Normal content, slightly overlapping cards1 to 10
Header or navigation bar pinned to the top100
Dropdown menu, tooltip500
Modal window, full-screen overlay1000


Frequently asked questions

Question

I set z-index: 9999 and nothing changes, why?

The first thing to check is the element's position: without a value other than static, z-index stays inactive no matter how large it is. Raising the number further never fixes this specific problem, only the position property can.


Question

Can z-index be negative?

Yes, and it is a common technique for placing a decorative element just behind its parent, for example a background gradient that slightly overflows a card. A negative z-index pushes the element behind everything else in the same stacking context, including the parent's own background.


Question

Why does my child stay behind an element with a smaller z-index?

Because an ancestor has probably created a new stacking context without anyone deciding to, often through an opacity below 1, a transform, or a filter. The child's z-index is then only compared against other elements in that same group, never directly against elements outside it.

Related terms

Discover our hTML and CSS glossary

Browse the terms and definitions most commonly used in HTML and CSS development.

Share this article

Want to help us? Share this article on your networks or even better: on your site, in an article or in your newsletter.