CSS Common Mistakes and Debugging CSS
Every CSS author eventually hits the same wall: a rule that should work simply doesn’t. The text is the wrong color, a box refuses to line up, or an element vanishes behind another one for no obvious reason. CSS rarely throws an error message when something goes wrong — it just silently applies a different result than you expected. This lesson teaches you how to systematically debug CSS using browser DevTools, and walks through the mistakes that cause the vast majority of real-world CSS bugs.
Overview: Why CSS Bugs Are Different From Other Bugs
Unlike JavaScript, CSS has no exceptions, no stack traces, and no console errors for most mistakes. If you misspell a property name, use an invalid value, or write a selector that matches nothing, the browser doesn’t stop and complain — it simply ignores the offending declaration or rule and moves on. This “fail silently” behavior is a deliberate part of the CSS specification (it lets browsers add new properties without breaking pages in older browsers), but it means the burden of finding mistakes falls entirely on you and your tools.
Three mental models explain almost every CSS bug you’ll encounter:
- The cascade and specificity — when two rules target the same element and property, the browser has strict, deterministic rules for which one wins (origin and importance, then specificity, then source order). If the “wrong” rule wins, it’s not random — it’s math you can calculate.
- The box model — every element is a box made of content, padding, border, and margin. Sizing and overflow bugs almost always come from misunderstanding how these layers add together, especially with the default
box-sizing: content-box. - Stacking contexts and layout algorithms — properties like
position,z-index,transform, andopacitycan create new stacking contexts or formatting contexts that change how children behave, often surprising developers who only look at the element itself and not its ancestors.
Debugging CSS is the skill of turning “this looks wrong” into “here is the exact rule, computed value, or layout step responsible” — and browser DevTools are built specifically to answer that question.
Syntax: The Debugging Toolkit
There isn’t a single “debugging syntax” in CSS the way there is for a selector or a property, but there is a standard workflow and a handful of techniques you’ll reuse constantly. The most useful is the outline trick: temporarily give every element a visible outline so you can see box boundaries without disturbing layout.
* {
outline: 1px solid red;
}
*— the universal selector, matching every element on the page.outline— chosen instead ofborderbecause outlines are drawn outside the box without affecting layout or triggering the box model’s size calculations, so they never shift anything on screen.1px solid red— a thin, high-contrast line that’s easy to spot but not visually overwhelming.
Beyond ad-hoc rules like this, your primary debugging tool is the browser’s DevTools, opened with F12 or by right-clicking an element and choosing “Inspect”. The panels you’ll use most:
| Panel | What it tells you |
|---|---|
| Elements / Styles | Every rule matching the selected element, in cascade order, with overridden rules shown crossed out |
| Computed | The final, resolved value of every property after the cascade has been applied |
| Box model diagram | Exact pixel values for content, padding, border, and margin on the selected element |
| Layout / Grid overlay | Visual overlay of flex or grid tracks, gaps, and alignment lines |
Examples
Example 1: Finding invisible box boundaries
Apply the universal outline rule to any page where spacing looks off:
* {
outline: 1px solid red;
}
Result: Every element on the page — including ones with no visible border or background — gets a thin red outline drawn around its actual box, without shifting any content. Invisible wrapper <div>s, unexpectedly large margins, and elements that are taller or narrower than you assumed suddenly become obvious.
This works because outline is painted outside the border edge and, unlike border, never takes up space in the box model or affects sibling positioning. You can drop this rule in, look around, and delete it without any risk of it having changed your layout while you were debugging.
Example 2: A box model overflow bug
A card is supposed to be exactly 300px wide, but it’s visibly wider than its container. Here’s the CSS causing it:
.card {
width: 300px;
padding: 20px;
border: 2px solid #333;
}
Result: The card actually renders at 344px wide (300px content + 20px left padding + 20px right padding + 2px left border + 2px right border), overflowing a 300px-wide parent container by 44px.
Selecting the element in DevTools and looking at the box model diagram in the Computed panel would show exactly this breakdown: a 300px content box, then padding and border layered outside it. This happens because the default value of box-sizing is content-box, meaning width only sets the content area — padding and border are added on top. The fix:
.card {
width: 300px;
padding: 20px;
border: 2px solid #333;
box-sizing: border-box;
}
Result: The card now renders at exactly 300px total width. With box-sizing: border-box, the browser shrinks the content area to make room for padding and border, so width describes the final rendered size instead of just the content.
Example 3: A stacking context bug
A modal with a very high z-index still renders behind the page header:
.dashboard {
transform: translateZ(0);
}
.dashboard .modal {
position: fixed;
z-index: 9999;
}
.header {
position: sticky;
top: 0;
z-index: 100;
}
Result: Even though .modal has z-index: 9999 versus the header’s z-index: 100, the modal still appears underneath the header.
The cause is subtle: applying transform to .dashboard creates a new stacking context on that element. Because .modal is a descendant of .dashboard, its z-index is only compared against other elements inside that same stacking context — it can never climb above .dashboard itself in the stacking order, regardless of how large its z-index value is. DevTools’ Layers panel (or the small badge next to elements that create stacking contexts) reveals this. The fix is to remove the unnecessary stacking-context trigger from the ancestor:
.modal {
position: fixed;
z-index: 9999;
}
.header {
position: sticky;
top: 0;
z-index: 100;
}
Result: With no transform on .dashboard, .modal‘s stacking context is the root of the page, so its z-index: 9999 is compared directly against .header‘s z-index: 100 and the modal now correctly renders on top.
How Debugging Works Step by Step (Under the Hood)
When you inspect an element and see a property crossed out in the Styles panel, DevTools is showing you the result of the browser’s cascade algorithm, computed in this order:
- Collect all matching rules. The engine finds every rule whose selector matches the element, from every stylesheet and origin (user-agent defaults, author styles, inline styles).
- Sort by origin and importance. Author styles beat user-agent defaults;
!importantdeclarations are moved into their own, higher-priority bucket and compared separately from normal declarations. - Compare specificity within the same bucket. Specificity is calculated as a triple of (ID selectors, class/attribute/pseudo-class selectors, type/pseudo-element selectors). For example,
#nav .itemscores (1, 1, 0) and beats.nav .item.active‘s (0, 2, 0) — wait, actually the reverse: (0,2,0) beats (1,1,0) is false since ID always outweighs any number of classes; the ID selector wins regardless of how many classes the other rule has. - Fall back to source order. If specificity ties exactly, whichever rule appears later in the stylesheet (or later `