Debugging CSS
Every CSS bug eventually reduces to one of a small number of causes: specificity, a typo, cascade order, or a layout model you did not expect (flexbox default sizing, stacking contexts, box-model math). A systematic process gets you to the actual cause faster than guessing and randomly changing values until something looks right.
Step 1: the outline trick
Before anything else, make every box visible. This single line reveals unexpected margins, overlapping elements, and mystery whitespace instantly.
* {
outline: 1px solid red;
}
/* Or, to see nesting depth at a glance: */
* {
outline: 1px solid hsl(calc(var(--depth, 0) * 40) 80% 50%);
}* selector, or just run it from the console: document.styleSheets[0].insertRule("* { outline: 1px solid red }").Step 2: the DevTools inspect flow
Right-click the element and choose Inspect (or use the element-picker icon, top-left of the Elements panel)
Check the Styles pane: rules are listed most-specific/most-recent first, with strikethrough on any overridden declaration
Look for a strikethrough on the property you expect to apply — that tells you something else is winning
Click the source link next to a rule to jump straight to the stylesheet and line number that defined it
Step 3: the Computed panel
The Styles pane shows what was written; the Computed panel shows what actually won after the entire cascade resolved. When you are not sure which rule is in effect, Computed is the ground truth.
Computed panel shows the final, resolved value for every property on the selected element
Click the arrow next to a computed value to see exactly which rule (and which stylesheet/line) provided it
Use the filter box at the top of Computed to jump straight to one property (e.g. type "margin")
Step 4: finding overflow culprits
A horizontal scrollbar appearing unexpectedly almost always means one element is wider than its container. Binary-search for it instead of guessing.
/* Quickly find which element is overflowing horizontally */
* {
outline: 1px solid red;
}
/* Or, more surgically, in the DevTools console: */
/* [...document.querySelectorAll('*')].forEach(el => {
if (el.scrollWidth > document.documentElement.clientWidth) {
console.log(el);
}
}) */max-width: 100%, and negative margins pulling content past the viewport edge are the most frequent causes of unexpected horizontal scroll.Step 5: layout shift debugging
Open DevTools → Performance panel → record a page load or interaction
Look for "Layout Shift" entries in the timeline; clicking one highlights exactly which element moved
Lighthouse also reports Cumulative Layout Shift (CLS) with the specific elements responsible
Common causes: images/ads without reserved space, web fonts swapping in in without
font-displaytuning, content injected above existing content
Step 6: bisecting with display: none
When you cannot tell which of several elements is causing a layout problem, hide them one at a time (or half at a time, for a true binary search) until the problem disappears — the last element you hid is the culprit.
/* Toggle in DevTools: select the element, press "H" (Chrome/Firefox
shortcut) to instantly toggle display: none without editing CSS */
.suspect-element {
display: none !important; /* temporary, for debugging only */
}H toggles its visibility instantly (Chrome and Firefox both support this) — much faster than manually adding a display: none rule and removing it afterward."Why isn't it applying?" — the usual suspects
Symptom | Likely cause | How to confirm |
|---|---|---|
Rule shows strikethrough in Styles panel | A more specific (or later) rule overrides it | Check the Computed panel to see which rule actually won |
Rule does not appear in Styles panel at all | Typo in the selector, or the class/id is not actually on the element | Search the Elements panel for the exact class name to confirm it is present |
Property is set but has no visible effect | Wrong property for the display type (e.g. | Check the element's computed |
Rule is defined but never wins | A | Check the Styles panel for a layer name next to the rule |
Style works in one place, not another | Cascade layers, media query mismatch, or a container query condition not met | Check the conditions listed above the rule in the Styles panel |
Debugging cascade layers
Cascade layers (@layer) change the usual rule that "higher specificity always wins" — a layer declared later beats an earlier layer regardless of specificity, which surprises people the first time they hit it.
@layer reset, base, components, utilities;
@layer base {
.button { color: blue; }
}
@layer utilities {
/* Wins even though .button here has LOWER specificity than
an id selector in an earlier layer, because utilities is
declared later in the @layer order */
.button { color: red; }
}Debugging container queries
A @container rule that never seems to trigger is almost always a missing container-type declaration on the intended ancestor — without it, the element is never established as a query container, and the query silently never matches.
.sidebar {
container-type: inline-size; /* REQUIRED for @container to work */
container-name: sidebar;
}
@container sidebar (min-width: 300px) {
.widget {
grid-template-columns: 1fr 1fr;
}
}Confirm
container-typeis actually set on the intended ancestor — check the Computed panel for that specific elementIn the Elements panel, container-query-aware DevTools show a small "container" badge on elements that qualify, similar to the flex/grid badges
Resize the DevTools viewport (not the browser window) to trigger a container query bound to viewport-relative container size, and watch the badge/overlay update
Debugging custom properties
A custom property that "does nothing" is usually either misspelled, or its value was never actually set on an ancestor the way you assumed. The Computed panel resolves the mystery by showing the actual final value the var() call received.
:root {
--brand-color: #2563eb;
}
.button {
color: var(--brand-colour, black); /* typo: "colour" vs "color" --
silently falls back to black */
}Debugging on a real mobile device
Some bugs (safe-area insets, real touch behavior, actual viewport height quirks) only show up on a physical device. Both Chrome and Safari support remote debugging a connected phone directly from desktop DevTools.
Connect the phone via USB and enable developer/remote debugging in its browser settings
Open chrome://inspect (Chrome) or Safari's Develop menu (Safari) on the desktop
Select the connected device's open tab to get a full desktop DevTools session mirroring the real device
Use this for any bug report that only reproduces "on my phone" — most such bugs come down to safe-area insets, dynamic viewport units, or touch-specific interaction states