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”.

Reading origin in the DevTools Styles panel A simplified Styles panel. The top entry is an author rule annotated with the reset layer and a winning font declaration. Below it a user agent stylesheet entry shows the same property struck through, indicating it lost the cascade. Styles element.style { } button, input, select, textarea { font: inherit; } @layer reset · reset.css:12 WINS — author origin button { font: 400 13.33px Arial; } user agent stylesheet loses on origin, not on weight A struck-through UA declaration is the normal, healthy state — no !important required to produce it.

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.

Three outcomes when inspecting a browser-default conflict A starting question about which origin won branches into three outcomes: author rule wins with no action needed, no author rule matches which requires fixing the selector, and a rare important user-agent declaration that cannot be overridden. Which origin holds the winning rule? author, with a layer label healthy — no action UA, no author rule shown the selector does not match target the element itself in @layer reset UA declaration is !important rare, and unbeatable change the markup instead

Verification

  1. Re-inspect the element. The UA declaration should now appear struck through, with your reset rule winning above it.
  2. Confirm a higher layer can still override the reset — add a temporary font-family in utilities and check it takes effect. If it does not, an !important is still present somewhere.
  3. 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"
  1. 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

What a reset should correct — and what it must not touch Two columns inside the reset layer. The left lists browser defaults worth correcting: box-sizing, body margin, control fonts, image display. The right lists defaults that must be preserved: the focus ring, list semantics, reduced-motion behaviour and native control semantics. correct these never remove these box-sizing :focus-visible outline body { margin: 0 } list semantics on ul / ol font: inherit on controls prefers-reduced-motion img { display: block } native control semantics None of the left column needs !important — author origin already outranks the browser.
/* ============================================================
   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.

Up: Cascade Origins & User Agent StylesSpecificity Management & Conflict Resolution