AniUI Academy

Debugging Common Layout Bugs

A checklist of common CSS layout bugs — margin collapsing, flex/grid overflow, whitespace gaps, dead z-index, clipped shadows, and box-sizing mismatches — and how to fix them.

10 min read

This is the last lesson in the track, and it's deliberately different from the ones before it: instead of introducing a new property, it's a reference for the handful of layout bugs that show up constantly in real work, regardless of which specific layout technique you're using. Each one has a concrete, verifiable cause — not a vague "CSS is weird" shrug — and a specific fix. Bookmark this one.

Bug: "why is there only one margin's worth of gap here?"

You already met this behaviour in the box-model lesson; here it is again, now as a debugging entry rather than new material. Two stacked block elements, each with vertical margin:

p {
  margin-top: 1rem;
  margin-bottom: 1rem;
}
<p>First paragraph.</p>
<p>Second paragraph.</p>

The gap between the two paragraphs is 1rem, not 2rem. Adjacent vertical margins between block-level elements collapse into a single margin equal to the larger of the two (or that same value, if they match) — they don't add together. This is standard, spec-defined behaviour, not a bug in the literal sense, but it surprises almost everyone the first time they measure a gap and get half of what they expected.

Fix / avoidance: if you want a gap that behaves predictably regardless of collapsing, use gap on a flex or grid container instead of relying on margins between siblings — gap never collapses, because it isn't margin at all. If you're stuck using margins, remember only vertical margins between block-level, in-flow siblings collapse — margins don't collapse across a flex or grid container's direct children, and horizontal margins never collapse.

Bug: a flex/grid child overflows despite flex-shrink

.row {
  display: flex;
  gap: 1rem;
}
.row > * {
  flex: 1;
  flex-shrink: 1; /* should let this shrink... */
}

A long, unbreakable piece of content (a long URL, an unbroken filename) inside one of these flex children still overflows the container, pushing past its boundary, even though flex-shrink is set and should, in theory, let the item shrink to make room.

The real, verified cause: flex items have a default min-width: auto (or min-height: auto for a column-direction flex container) — not 0. min-width: auto resolves to the content's own intrinsic minimum width, which for an unbreakable string of text is the width needed to fit that whole string on one line. That default floor takes priority over flex-shrink's ability to shrink the item smaller, no matter how high you set flex-shrink.

Fix:

.row > * {
  flex: 1;
  min-width: 0; /* removes the auto floor, letting flex-shrink actually work */
}

Explicitly setting min-width: 0 (or min-height: 0 in a column flex context) removes that intrinsic-content floor, and the item can then shrink as far as flex-shrink allows — typically down to the point where its own content starts wrapping or getting an ellipsis via overflow / text-overflow. The same default and the same fix apply to grid items too.

Bug: mystery gaps between inline-block elements

.icon {
  display: inline-block;
  width: 32px;
  height: 32px;
}
<span class="icon"></span>
<span class="icon"></span>
<span class="icon"></span>

Three icons that should sit flush against each other instead have a small visible gap between them — usually a handful of pixels, roughly the width of a space character in that font.

The real cause: inline-block elements are laid out like words in a sentence, and HTML's whitespace-collapsing rule (from the document-structure lesson) still applies — but it collapses whitespace to a single space, it doesn't remove it. The newline and indentation between </span> and the next <span> in your source collapses down to exactly one rendered space character's worth of gap, which shows up visually between the elements.

Fix options, roughly in order of how commonly you'll reach for them:

/* Option 1: switch the parent to flex — gaps between inline-block
   elements are a flow-layout quirk that flex doesn't have at all */
.icon-row { display: flex; }
<!-- Option 2: remove the whitespace in the source, e.g. by
     closing and opening tags on the same line with no space between -->
<span class="icon"></span><span class="icon"></span><span class="icon"></span>
/* Option 3: zero out the parent's font-size, since the gap's
   width is based on the current font's space-character width */
.icon-row { font-size: 0; }
.icon { font-size: 1rem; } /* restore it on the icons themselves if they contain text */

In real modern layouts, reaching for flexbox (option 1) is usually simplest — this whole bug is really a symptom of using inline flow layout for something that's actually a row of boxes, which is exactly the kind of problem flexbox was built to solve.

Bug: z-index has no effect

.tooltip {
  z-index: 999; /* has no effect at all */
}

The tooltip still renders behind other elements despite an aggressively high z-index.

The real cause: z-index is only defined to have an effect on a positioned element — meaning its position is something other than the default static. On a position: static element (the default for everything unless you set it otherwise), z-index is simply ignored, no matter how large the value.

Fix:

.tooltip {
  position: relative; /* or absolute/fixed/sticky, whichever the layout needs */
  z-index: 999; /* now it actually applies */
}

Any positioned value works — relative, absolute, fixed, or sticky — the requirement is just "not static." This is one of the single most common "why isn't my z-index working" causes, and checking position first, before adjusting the z-index number at all, saves a lot of trial and error.

Bug: a box-shadow gets clipped

.card {
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.card-container {
  overflow: hidden;
}

The card's shadow is visibly cut off in a hard, straight line instead of fading out naturally.

The real cause: overflow: hidden clips anything crossing its own box's edge, and a box-shadow — even though it visually reads as "outside" the element casting it — is still, technically, part of that element's rendered output. If a parent has overflow: hidden, any shadow on a child that extends past the parent's boundary gets clipped along with it, exactly like an oversized image would be.

Fix: the shadow needs a parent that doesn't clip. Depending on why overflow: hidden was there in the first place, options include moving it to a different, non-shadow-casting wrapper, adding padding to the container so the shadow has room to render before it would reach the clipped edge, or removing overflow: hidden if it was only there for an unrelated reason and isn't actually needed.

Bug: an element is wider than expected (box-sizing mismatch)

.input {
  width: 300px;
  padding: 12px;
  border: 1px solid #cbd5e1;
}

This renders wider than 300px — closer to 326px once padding and border are included — which is surprising if you expected width: 300px to be the element's final, total rendered width.

The real cause: the default box-sizing is content-box, where width sets only the content area, and padding and border are added on top of that. This was covered in the box-model lesson as the default behaviour to be aware of — this entry is the "I forgot and it bit me" version.

Fix: most real projects set box-sizing: border-box globally, once, so width describes the element's total rendered width including padding and border — which almost always matches what you actually meant when you wrote width: 300px in the first place:

*, *::before, *::after {
  box-sizing: border-box;
}

Try it yourself

Every element in this playground has one bug from above; each is labelled and the fix is commented out beside it. Try enabling one fix at a time (delete the /* and */ around it) and re-running to see the difference.

Try it yourself
Loading playground...

What to remember — closing the course

Across this whole track, the layout mental model that actually matters boils down to a few ideas, and every bug above is really just one of them showing up in a specific disguise:

  • Every element is a box, and its final rendered size depends on box-sizing — know which one you're in, or set border-box globally and stop guessing.
  • Flow layout has its own inherited quirks — margin collapsing and inline-block whitespace gaps both come from rules that predate flexbox and grid, and both usually disappear the moment you switch the parent to flex or grid instead of fighting the older flow-layout behaviour.
  • Flexbox and grid have their own defaults that surprise you until you know themmin-width: auto/min-height: auto on flex/grid items is the single most common cause of "why won't this shrink," and it has one reliable fix: an explicit min-width: 0 or min-height: 0.
  • Stacking and clipping are governed by rules, not vibesz-index only does anything on a positioned element, and overflow: hidden clips everything crossing its box, shadows included, whether or not that feels intuitive.
  • When something doesn't make sense, open devtools before guessing. The Styles pane, the box model diagram, and the Computed tab from the previous lesson answer almost every "why is this happening" question directly, faster than reasoning about it from memory.

That's the mindset this whole track has been building toward: a real page's layout is not magic and it is not arbitrary — it's a specific, learnable set of rules, and once you know where to look, every "weird" layout bug turns out to have a completely ordinary explanation.

Check yourself

4 questions · pass 3/4 to finish the course

up to 50
  1. 1.Two stacked <p> elements each have margin-top and margin-bottom of 1rem. How much gap actually appears between them?

  2. 2.A flex child with flex-shrink:1 still overflows its container despite content that could visually shrink. What's the real, verified cause?

  3. 3.A z-index on an element has no visible effect at all. What is the single most common reason?

  4. 4.A box-shadow on a child element is visibly cut off in a straight line. What's the usual cause?

4 left to answer