How User Agent Styles Interact With Cascade Layers
Somebody adds font-family: var(--font-body) !important to a reset because “the browser keeps winning on buttons”, and the important declaration then blocks every legitimate override for the next two years. The browser was never winning on priority — it was winning because no author rule matched. This audit shows how to tell those two situations apart in under a minute, applying cascade origins and user agent styles from the specificity management reference.
Prerequisites
You need to know that origin is compared before cascade layer, so every author declaration — in any layer — outranks every normal user-agent declaration. If that ordering is not yet familiar, read the parent cluster first. The canonical stack is assumed:
/* WHY: `reset` is the lowest author layer and still outranks the browser.
That single fact makes every !important in a reset unnecessary. */
@layer reset, base, theme, components, utilities;Tooling: Chrome, Edge, Firefox or Safari DevTools. No build step.
Step 1: Turn on user-agent styles
The default DevTools view hides the browser’s own declarations, which is why this class of bug is so often misdiagnosed.
- Chrome / Edge: Settings (F1) → Preferences → Elements → check Show user agent styles.
- Firefox: Inspector → Rules pane → the three-dot menu → Show Browser Styles.
- Safari: Elements → Styles → the gear icon → Show User Agent Stylesheet Rules.
What this does: makes UA declarations visible in the Styles panel, so “no rule is shown” stops being ambiguous between “no rule exists” and “the panel is filtering it out”.
Step 2: Read the origin of the winning declaration
Select the element and find the property in the Styles panel. Three outcomes, three different diagnoses:
/* OUTCOME A — an author rule wins, annotated with its layer.
Nothing to fix; the cascade is behaving as designed. */
/* OUTCOME B — a UA rule wins and NO author rule appears for the property.
The author selector does not match. This is a selector bug. */
/* OUTCOME C — a UA rule wins while an author rule is visible and NOT
struck through. This means the UA declaration is !important, which is
rare but real. */What this does: replaces guessing with a three-way classification. Outcome B is by far the most common, and it is the one that gets misdiagnosed as a priority problem.
Step 3: Confirm whether an author rule matches at all
The classic case is form-control typography, because those UA declarations sit directly on the control and inheritance never reaches them.
@layer base {
/* WHY: this styles the body — and buttons do NOT inherit font from it,
because the UA sets `font` on `button` explicitly. The rule below is
matching, but not on the element you care about. */
body { font-family: var(--font-body); }
}@layer reset {
/* WHY: the correct fix — target the controls themselves so an author
declaration exists for the browser's to lose to. No !important, because
author origin already beats user-agent origin. */
button, input, select, textarea, optgroup {
font: inherit;
color: inherit;
letter-spacing: inherit;
}
}What this does: creates the matching author declaration that outcome B was missing. The panel immediately flips to outcome A: the UA rule appears struck through.
Step 4: Move the correction into reset and delete the !important
Corrections to browser defaults belong in the lowest layer, so every other layer can still override them.
/* BEFORE — in a component file, escalated twice over.
@layer components {
.form .btn { font-family: var(--font-body) !important; }
}
WHY THIS IS WRONG: the !important buys nothing against the UA (author
already wins), and it blocks the utilities layer from ever changing the
font — the exact override the architecture exists to allow. */
@layer reset {
/* AFTER — one rule, lowest layer, no importance, applies everywhere. */
button, input, select, textarea { font: inherit; }
}What this does: removes a blocker rather than adding a fix. Run the same conversion for every !important whose commit message or comment mentions the browser.
Verification
- Re-inspect the element. The UA declaration should now appear struck through, with your
resetrule winning above it. - Confirm a higher layer can still override the reset — add a temporary
font-familyinutilitiesand check it takes effect. If it does not, an!importantis still present somewhere. - Grep for the defensive pattern across the codebase and confirm the count is falling:
# Any !important on a property the browser also sets is a candidate for
# deletion — review each hit against the origin ladder.
grep -rn "!important" src/styles --include="*.css" | grep -E "font|margin|padding|appearance"- Check a native control specifically. Buttons, selects and file inputs are where UA styling is heaviest and where a missing correction shows up first.
Troubleshooting
The author rule is visible but struck through, and a UA rule wins.
: The UA declaration is !important. Nothing an author writes can override it; the fix is to change the element or attribute you are styling rather than the CSS.
A <select> ignores the reset entirely.
: Several parts of native select rendering are drawn by the platform, not by CSS, and no origin can reach them. Style the control’s box and accept the platform’s internals, or replace it with a custom listbox that carries the correct ARIA roles.
The reset works in Chrome but not Safari.
: The two engines’ UA stylesheets differ, most visibly on form controls and -webkit- appearance defaults. Test each correction in both, and prefer appearance: none plus explicit re-styling where the difference matters.
Removing an !important broke a page.
: Something in a higher layer was being suppressed by it, and that rule is now winning. That is information, not a regression — inspect the newly winning declaration and decide whether it belongs where it is.
An extension’s styles beat the reset. : Most extensions inject author-origin styles into the page, so they are unlayered and outrank every layer. Reproduce with extensions disabled before treating it as a site bug.
Complete working example
/* ============================================================
A reset that corrects the browser without fighting it
============================================================ */
@layer reset, base, theme, components, utilities;
@layer reset {
/* WHY: box-sizing is the one UA default almost every design system
disagrees with, and correcting it in the lowest layer means every
component can still opt out. */
*, *::before, *::after { box-sizing: border-box; }
/* WHY: the UA's body margin exists for unstyled documents. Removing it is
a correction, not an opinion, so it belongs here rather than in base. */
body { margin: 0; }
/* WHY: form-control typography does not inherit — the UA sets it directly
on the control. This is the single highest-value rule in any reset. */
button, input, select, textarea, optgroup {
font: inherit;
color: inherit;
}
/* WHY: images are inline by default, which produces the mysterious
descender gap under every figure. */
img, svg, video, canvas { display: block; max-width: 100%; }
/* WHY: NOT reset — the UA focus ring is an accessibility affordance.
Restyle it in theme if it clashes; never remove it here.
:focus-visible { outline: none; } ← deliberately absent */
}
@layer theme {
/* WHY: a visible, on-brand focus ring, layered ABOVE reset so it wins,
and below components so a component can still specialise it. */
:focus-visible {
outline: 2px solid var(--color-focus);
outline-offset: 2px;
}
}
@layer components {
/* WHY: no !important anywhere in this file — every rule here already
outranks the browser by virtue of being author CSS. */
.btn {
padding: var(--space-3) var(--space-6);
border-radius: var(--radius-md);
background: var(--color-primary);
color: var(--color-on-primary);
border: none;
}
}Frequently Asked Questions
Why does my button still use the browser font after I set one in a layer?
Because the user-agent stylesheet sets font directly on button, and font is not inherited past an explicit declaration. A rule on body or a wrapper never reaches the control, so the UA declaration is the only one matching that element — it wins by default rather than by priority. Add font: inherit on button, input, select, textarea in your reset layer and the problem disappears permanently.
Should a reset layer use !important to beat browser defaults?
No, and doing so causes a second problem. Author declarations already outrank user-agent declarations in every layer, so the importance buys nothing against the browser. What it does buy is a rule your own components and utilities layers can no longer override — a reset that cannot be overridden has stopped being a reset and become a constraint.
How do I see which user-agent rule is responsible for a value?
Enable the browser-styles preference in DevTools — Chrome and Edge put it under Settings → Preferences → Elements, Firefox under the Rules pane menu, Safari under the Styles gear icon. UA declarations then appear inline in the Styles panel labelled user agent stylesheet, and the Computed tab names the rule that produced each resolved value, which is the fastest way to attribute a value you did not write.
Related
- Cascade Origins, User Agent and User Styles — the origin ladder this audit applies
- Overriding Browser Form Control Defaults in a Base Layer — the form-control corrections in full
- Why Your CSS Reset Isn’t Working With Cascade Layers — the sibling failure mode inside the author origin
- Debugging Unlayered Author Styles in DevTools — what to check once origin is ruled out
Up: Cascade Origins & User Agent Styles → Specificity Management & Conflict Resolution