Every element the browser paints is a rectangle, and every rectangle is computed by the same deterministic algorithm — the box model. Most CSS bugs that feel mysterious (an element that's '10px too wide,' a dropdown trapped behind a modal, a margin that silently vanishes) aren't mysterious at all once you know which of two well-defined systems is responsible: box-model sizing, or stacking-context painting order. They're separate systems, they don't share rules, and conflating them is where most layout debugging time gets wasted.


The Box Model: Four Layers, One Sizing Algorithm

Content, padding, border, and margin nest outward from the center, and the box-sizing property decides which of them count toward the width/height you declared. This isn't a stylistic preference — it changes what number the layout engine actually uses when computing how much horizontal space a box consumes in its container.

The Four Layers, Inside Out

  • Content: the actual text, image, or nested elements — this is the only layer content-box counts toward width/height.
  • Padding: transparent space inside the border, pushed outward from the content — always added, regardless of box-sizing.
  • Border: a visible or invisible line drawn at the padding's outer edge — its thickness is included in border-box sizing, excluded in content-box.
  • Margin: transparent space outside the border, used to separate a box from its siblings — never part of the box's own size, always part of the layout gap around it.
Box Model Layersmarginborderpaddingcontentcontent-box (default)width: 300px+ padding 20px each side+ border 1px each siderendered width: 342pxborder-boxwidth: 300pxpadding + border subtractedfrom the content area insteadrendered width: 300pxSame declared width. Different final size. This is why *, *::before, *::after { box-sizing: border-box } is nearly universal.
content-box adds padding and border on top of the declared width. border-box subtracts them from it. Same CSS property, two different final sizes.
reset.csscss
/* Apply border-box universally so declared widths are the final,
   predictable rendered widths -- the near-universal first line of any reset. */
*, *::before, *::after {
  box-sizing: border-box;
}

.card {
  width: 300px;        /* total rendered width is exactly 300px */
  padding: 1rem;        /* carved out of the 300px, not added on top */
  border: 1px solid var(--color-border, #cbd5e1);
}

Margin Collapse: Why 32px + 24px Isn't 56px

Adjacent vertical margins between block-level elements in normal flow collapse into a single margin equal to the larger of the two, not their sum — this is a deliberate part of the CSS spec, not a bug, and it also applies between a parent and its first or last child when nothing (no border, padding, or content) separates them. Flexbox and grid containers opt out of this behavior entirely for their children, which is one of the quieter reasons layouts feel more predictable after a migration from float-based layout to flex/grid.

margin-collapse.csscss
.section { margin-bottom: 32px; }
.heading { margin-top: 24px; }
/* Gap between .section and .heading is 32px (the larger value), not 56px. */

.flex-container { display: flex; flex-direction: column; }
/* Inside a flex container, margins between children NEVER collapse --
   .section's 32px and .heading's 24px both apply in full: 56px gap. */

Stacking Contexts: Why z-index: 9999 Sometimes Does Nothing

z-index does not compare globally across the whole page. It only compares siblings within the same stacking context, and a stacking context is created implicitly by properties that have nothing to do with z-index at all — position: relative combined with a set z-index, opacity less than 1, transform, filter, will-change, and several others each independently create a new one. Once an element creates a stacking context, everything painted inside it is sealed as a unit: no descendant's z-index, however large, can escape above an element outside that context that has a higher stacking order.

This is the actual mechanism behind a bug every frontend engineer hits eventually: a dropdown menu with z-index: 9999 rendering behind a modal with z-index: 1000. The dropdown's 9999 only wins within its own parent's stacking context — if that parent has transform: translateZ(0) for a GPU-accelerated animation, or opacity: 0.99 left over from a fade transition, the parent itself is now a sealed context competing against the modal's context at a lower level, and the dropdown's inflated z-index never gets compared to the modal at all.

Root Stacking Context.card { transform: translateZ(0); }creates a NEW sealed stacking context.dropdown { z-index: 9999; }only compares against siblingsINSIDE .card's contextnever reaches the root level.modal { z-index: 1000; }lives directly in the root contextwins against .card's entire sealedsubtree, dropdown includedThe dropdown's z-index: 9999 loses to the modal's z-index: 1000 -- the numbers were never compared at all.
z-index only competes within its own stacking context. A transform, opacity, or filter on an ancestor silently seals off everything below it from the rest of the page's stacking order.
Any of these on an ancestor silently caps how far a descendant's z-index can reach — this is the first thing to check when z-index 'isn't working.'
Property / SituationCreates a New Stacking Context?
position: relative/absolute with z-index setYes
position: fixed or stickyYes, always, regardless of z-index
opacity less than 1Yes
transform, filter, or will-change set to a non-default valueYes
display: flex or grid item, no other properties setNo, by itself
A plain <div> with only color/background setNo

Semantic HTML: Accessibility and SEO You Get for Free

A <div onclick=...> and a <button> can look pixel-identical and still be functionally unequal: the <button> is keyboard-focusable, triggers on both click and Enter/Space without extra JS, exposes a button role to screen readers automatically, and participates correctly in form submission. Every one of those is a browser-native behavior tied to the element's semantics -- reproducing them on a <div> requires tabindex, explicit keydown handling, and manual ARIA role/state management, and it's easy to miss one.

semantic-vs-div.htmlhtml
<!-- Works visually, broken for keyboard users and screen readers
     unless every one of these is added back by hand: -->
<div class="btn" onclick="submitForm()" tabindex="0"
     role="button" onkeydown="if(event.key==='Enter'||event.key===' ')submitForm()">
  Submit
</div>

<!-- All of the above, free, from the element itself: -->
<button type="submit">Submit</button>

<!-- Same logic applies to page structure -- a screen reader's landmark
     navigation and a search crawler's content model both read this: -->
<article>
  <header><h1>Article Title</h1></header>
  <section aria-labelledby="intro-heading">
    <h2 id="intro-heading">Introduction</h2>
    <p>...</p>
  </section>
</article>
<!-- versus an unstructured stack of <div class="title">, <div class="section">
     that carries zero semantic meaning to anything but a human eye. -->

What Semantic Markup Buys You Without Extra Work

  • Keyboard operability by default: <button>, <a href>, and form controls are focusable and activatable via keyboard with zero JavaScript, which a div-with-onclick never gets without manually reimplementing it.
  • Screen reader landmark navigation: <nav>, <main>, <header>, <footer>, and <article> let assistive tech users jump directly between page regions instead of tabbing through the entire DOM linearly.
  • Search engine content modeling: crawlers weight heading hierarchy (h1 through h6) and semantic sectioning when inferring a page's topic and structure -- a div-soup page with identical visible text can rank differently for this reason alone.
  • Free correctness on edge cases: a native <button disabled> can't be clicked or focused at all, which is one attribute versus reimplementing disabled-state handling across click, keydown, and ARIA on a div.

Closing: Two Separate Systems, One Rendering Pipeline

Box-model sizing and stacking-context painting are independent systems that both happen to be configured through the same CSS properties, which is exactly why they get conflated when debugging. Sizing bugs are almost always a box-sizing or margin-collapse question; visibility/ordering bugs are almost always a stacking-context question -- knowing which category a given bug belongs to before touching a single value is the difference between a thirty-second fix and an hour of increasing z-index numbers that were never going to work.

Semantic HTML sits underneath both, unrelated to either system directly, but it's the layer most often skipped under deadline pressure because a div-based layout looks correct in the browser you're testing in. It doesn't look correct to a screen reader, a keyboard-only user, or a search crawler -- and unlike a stacking-context bug, that failure mode doesn't announce itself with a visible glitch, which is exactly why it's worth building the habit of reaching for the semantic element first.