Debugging CSS in DevTools: Specificity Conflicts, Struck-Through Rules, and Inherited Mystery
Most CSS debugging is not knowledge of the spec; it is knowing where the browser tells you the answer. Every item below is readable in the panel without changing a line of code.
Read the cascade in the order the browser applies it
In DevTools, Styles shows matched rules bottom-up in source order but the browser applies them top-down in cascade order — the confusing part is that DevTools sorts by specificity in some views and by source order in others. Set the filter to the source-order view (⋮ → "Show all" plus source order) so the rule nearest the top of the list is the one that lost, not the one that won.
Above the rules list are the filter checkboxes: User-agent, User, :hover/:focus, Inherited, :root. Ticking "Inherited" is how you find out that line-height came from html and not from your component. Untick :hover when a rule looks applied but is not — a struck-through :hover rule is struck because the element is not hovered while you inspect it, not because the selector is wrong.
Why the value you wrote is not the value you got
| Symptom | Check | Usual cause |
|---|---|---|
| Rule struck through | the rule above it that also sets the property | higher specificity, or later in source at equal specificity |
| Rule struck through, no competitor visible | tick User-agent | a UA rule (input { } styles) is more specific than you think |
| Property greyed, value present | Computed panel, expand the property | invalid value for that property (height: 100% in an indefinite parent) — the declaration is dropped, not overridden |
| Value present but wrong units | Computed vs. used value | dvh/vh, fr → px, rem → px after a root font-size change |
| Nothing sets it | the Inherited row | it is inherited; color and line-height come from an ancestor |
| Works until you resize | hover the element and watch the layout highlight | container/media query, not the selector |
The single most under-used row is Computed → "Rendered". It shows used values after layout, which is the only way to see that height: 100% computed to 0 because the parent had no definite height, and that flex-basis: auto resolved to content width.
The three-line diagnosis for "it is overridden and I cannot find where"
- Select the element, Computed panel, type the property into the filter box.
- The panel prints the winning rule and its selector — click it to jump to source, including inside a bundled stylesheet.
- If the winner is a UA or browser-extension rule, tick User-agent/User in the Styles filter; extension stylesheets are the most common "phantom override" on a machine that is not yours.
Only after step 3 does !important enter the conversation. The correct fix for losing to a third-party library is a @layer at the right priority or a lower-specificity selector via :where(), not a heavier hammer — see resets and layers and the specificity table.
Layout problems are visible, not computable
- Hover a grid/flex container in Elements: the overlay draws the tracks and gaps. An "item too wide" bug is usually one track wider than you think, which the overlay shows and the Computed panel does not.
- Click a flex item and read
min-width:autois the reason a long string cannot shrink and the row overflows. See grid vs flexbox. - The layers icon (Layout pane) shows scroll containers, sticky elements, and stacking contexts. A dropdown buried under a "lower" element is a stacking-context problem and the icon shows the context boundary directly — no reading of
z-indexvalues required. - Paint/flame overdraw in the Performance panel: purple/green overdraw on a page that should be flat means overlapping opaque backgrounds, which is a real cost on a low-end device.
Reproduce it without the app
Right-click the element → Copy → Copy outerHTML, then paste into a blank tab with the stylesheet linked, or into the beautifier to make the extracted markup readable. Two reasons this matters: the bug you cannot reproduce in the app is usually a rule that only applies at one viewport width or after one JS state change, and a minimal reproduction answers "is it the framework or the CSS" in one step.
Two filters worth learning
In the Styles search (Ctrl+F inside DevTools, not the browser's): search a property name to find every rule setting it across all stylesheets — the fastest route to "who is setting overflow on everything". Search a value (z-index: 100) to find a magic number's origin when a component library has generated the selector for you.
What DevTools will not tell you
Which selector you intended. A rule that is absent from the matched list is absent because the selector does not match — a shadow boundary (::part() needed), an iframe, a <dialog> moved to the top layer, or a class name built at runtime with a typo. Check element.classList in the console before checking the cascade again.
Related: patterns and gotchas for the containment and margin-collapse behaviours these panels make visible, and minify safety for the case where the CSS you are debugging has already been rewritten by a build step.