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 area | Controlled by |
|---|---|
| Content | width, height, min-/max-width/height |
| Padding | padding (adds inside the border) |
| Border | border-width (visible edge) |
| Margin | margin (space outside, transparent) |
box-sizing value | What width measures |
|---|---|
content-box (default) | Content only; padding/border add to the total |
border-box | Content + padding + border together; total stays fixed |
Syntax
* {
box-sizing: border-box; /* declared width/height is the final total */
}Examples
/* 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
opacityortransformcreated a new stacking context, then debugging a "z-index bug" that isn't about z-index values at all. - Mixing
content-boxandborder-boxacross 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
transformandopacityare also the properties the browser can animate on the compositor thread without triggering layout, which is why they're preferred for animations over properties liketop/left.
Best Practices
- Apply
box-sizing: border-boxglobally 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.