Cloud Tech by Victor
FrontendBeginner

CSS Box Model & Stacking Context

Why box-sizing changes what "width" actually measures, and why a z-index of 9999 can still render below an element with z-index 5 once stacking contexts are involved.

Updated 2026-07-243 min read

Overview

Every element renders as nested rectangular boxes, content, padding, border, margin, from the inside out, and box-sizing decides which of those layers a declared width actually measures: content-box (the default) sizes only the content itself, so padding and border add to the total, while border-box makes the declared width the final total, shrinking the content area to fit padding and border inside it. Layering works on a separate, less intuitive system: z-index values are never globally comparable, they only mean something within the stacking context they belong to, and a long list of common properties, opacity under 1, any transform, filters, isolation: isolate, silently create a new one. That's why a z-index of 9999 can still render underneath a z-index of 5 elsewhere on the page, the two numbers were never being compared directly at all.

Quick Reference

Box areaControlled by
Contentwidth, height, min-/max-width/height
Paddingpadding (adds inside the border)
Borderborder-width (visible edge)
Marginmargin (space outside, transparent)
box-sizing valueWhat width measures
content-box (default)Content only; padding/border add to the total
border-boxContent + padding + border together; total stays fixed

Syntax

css
* {
  box-sizing: border-box; /* declared width/height is the final total */
}

Examples

css
/* A z-index of 9999 here still loses to a sibling element with
   z-index: 5 in the root stacking context, because opacity < 1
   silently created a new stacking context on the parent. */
.card {
  opacity: 0.99;
  position: relative;
}
.card .badge {
  position: relative; /* z-index has no effect on a statically positioned element */
  z-index: 9999;   /* only compared within .card's own stacking context */
}

A style change unrelated to z-index can still break layering

opacity, transform, filter, and several other properties create a new stacking context as a side effect. Adding a hover fade or a scale transition can silently change how an element's children layer against the rest of the page, with no z-index value involved in the actual cause.

Visual Diagram

Common Mistakes

  • Assuming z-index values are globally comparable numbers, when they only mean something within the same stacking context.
  • Not realizing a property like opacity or transform created a new stacking context, then debugging a "z-index bug" that isn't about z-index values at all.
  • Mixing content-box and border-box across a codebase, making width/height math inconsistent between components.
  • Forgetting that margin is transparent and doesn't take a background, then trying to color it directly instead of using padding or a wrapping element.

Performance

  • Box model calculations happen during layout and are effectively free relative to actual rendering cost; the real performance concern is triggering unnecessary layout recalculations (reflow), not the box model itself.
  • Stacking-context-creating properties like transform and opacity are also the properties the browser can animate on the compositor thread without triggering layout, which is why they're preferred for animations over properties like top/left.

Best Practices

  • Apply box-sizing: border-box globally so declared widths and heights stay predictable regardless of padding or border.
  • Treat z-index values as meaningful only relative to siblings in the same stacking context, never as globally comparable magic numbers.
  • Before "fixing" a layering bug by raising a z-index further, check whether an ancestor's opacity, transform, or filter created an unexpected stacking context boundary.
  • Keep the properties that create stacking contexts in mind when reviewing style changes near layered UI (modals, tooltips, dropdowns), since an unrelated visual-effect change can silently break assumed layering.

Interview questions

What does box-sizing: border-box actually change about how width and height are calculated?

With the default `content-box`, `width` applies only to the content area, so padding and border are added on top, a 200px-wide box with 20px padding and a 5px border renders at 250px total. `border-box` makes `width` include content, padding, and border together, so that same 200px `width` stays 200px total regardless of padding or border, the content area itself shrinks to make room instead. This is why most modern projects apply `box-sizing: border-box` globally, it makes an element's declared width predictable and stable as padding/border change, instead of recalculating the total size by hand every time.

An element has z-index: 9999 but still renders behind another element with z-index: 5. How is that possible?

z-index values are only compared within the same stacking context, they are not globally comparable numbers. If the z-index: 9999 element sits inside a parent that itself created a new stacking context (say, that parent has opacity less than 1, or z-index: 1), the entire parent, and everything inside it, is treated as a single unit when compared against sibling stacking contexts elsewhere in the document. A useful mental model is version numbers: an element at z-index 6 inside a parent context at z-index 4 effectively renders at "4.6", which is still below a sibling element at z-index 5 ("5.0") in the root context, no matter how high the inner z-index value looks in isolation.

Name three CSS properties, besides z-index with positioning, that create a new stacking context, and why does that matter when debugging a layering bug?

Opacity below 1, any non-none `transform`, and `filter` or `backdrop-filter` with a value other than `none` all create a new stacking context, along with several others like `isolation: isolate` and `will-change` naming a stacking-context property. This matters when debugging because a completely unrelated-looking style change, adding a fade transition via opacity, or a hover effect via transform, can silently create a new stacking context and change how that element's children layer against the rest of the page, a z-index layering bug that has nothing to do with z-index values themselves, but with an accidental new stacking context somewhere in the ancestor chain.

References

ShareXLinkedInReddit
Was this page helpful?
Suggest an improvement