Cascade Origins, User Agent and User Styles

A team migrates its reset into a reset layer, ships it, and someone immediately files a bug: “our lowest layer is going to lose to the browser defaults, right?” It never does — and the reason is worth understanding precisely, because the same rule explains why a user’s high-contrast stylesheet can beat everything you write. Layers order declarations inside one origin; origins are compared first, and that comparison happens before layer order is even consulted. This guide sits under specificity management and conflict resolution because origin confusion is the single most common cause of a conflict that “specificity can’t explain”.

Concept and spec reference

CSS Cascade Level 5 defines the cascade as a sort over declarations, applying these criteria in strict order: origin and importance, then context, then cascade layer, then specificity, then order of appearance. The first criterion is decisive — nothing lower in the list can rescue a declaration that lost on origin.

Three origins matter for day-to-day work:

  • User agent — the browser’s built-in stylesheet. Margins on body, display: block on div, the entire native form-control appearance.
  • User — styles the reader installs: a browser extension, a per-site custom stylesheet, an operating-system accessibility preference expressed as CSS.
  • Author — your stylesheets, inline style attributes, and everything a framework injects at runtime. All of your cascade layers live here, and only here.
/* WHY: this stack orders AUTHOR declarations only. Nothing in it can raise a
   declaration above the user origin, and nothing in it can sink a declaration
   below the user-agent origin — both boundaries are decided before layer
   order is consulted. */
@layer reset, base, theme, components, utilities;
The origin ladder for normal and important declarations Two columns. The left column orders normal declarations from user agent at the bottom up to author at the top, with the author band expanded to show the reset, base, theme, components and utilities layers. The right column shows important declarations in the inverted order, with user agent important at the top. Normal declarations !important declarations user agent origin user origin author origin — your layers reset base theme components utilities user agent !important user !important author !important Inside author !important the layer order inverts too: reset outranks utilities for important values. Nothing an author writes can beat an important user declaration — that is an accessibility guarantee.

Why the lowest layer still beats the browser

Because origin is compared before layer, reset — the weakest author layer — outranks every normal user-agent declaration. A single margin: 0 in reset defeats the UA’s margin: 8px on body without !important, without an ID selector, and without :where() tricks. The corollary is equally useful: if a browser default appears to be winning, the cause is never layer order. Either no author rule matches the element, or the property is one the UA sets with !important.

How the browser resolves an origin conflict

Walk a real conflict — a <button> styled by a design system:

/* User-agent stylesheet (paraphrased from Chromium's html.css) */
button { appearance: auto; font: 400 13.33px Arial; padding: 1px 6px; }

@layer reset {
  /* WHY: one author declaration in the weakest layer is enough to defeat the
     UA font — the origin comparison happens before layer order is read. */
  button { font: inherit; }
}

@layer components {
  .btn { padding: var(--space-3) var(--space-6); }
}
  1. Gather. For font, two declarations match: the UA rule and the reset rule. For padding, the UA rule and the components rule.
  2. Sort by origin and importance. Both author declarations are normal-priority and belong to the author origin; both UA declarations are normal-priority user-agent. Author wins both comparisons immediately.
  3. Sort by layer — only among the survivors. The UA declarations were already eliminated, so layer order never enters into it. Had two author layers both set font, this is where reset versus base would be decided.
  4. Specificity and order break any remaining tie inside a single layer.

The mental model that keeps this straight: layers are a sub-sort inside the author origin. They rearrange your own declarations relative to each other and have no reach beyond that boundary. For the rest of the sort, see how to calculate CSS specificity across multiple layers.

Cascade sort order — where layers enter the decision A vertical decision flow with five stages. Origin and importance is the first gate and eliminates cross-origin conflicts. Context follows. Cascade layer is the third gate and only sees declarations that already share an origin. Specificity and order of appearance are the final tiebreakers. 1 · Origin + importance 2 · Context 3 · Cascade layer 4 · Specificity 5 · Order eliminated here: UA normal rules user normal rules author !important only same-origin declarations remain, sorted reset → base → theme → components A declaration that loses at stage 1 is gone — no layer promotion can bring it back.

Practical usage patterns

Correction or opinion — which layer a rule belongs in Two columns. The reset layer holds neutral corrections any consumer would accept, such as box-sizing and font inherit on controls. The base layer holds house-style decisions such as appearance removal and typography scale, which a consumer might reasonably decline. @layer reset — corrections @layer base — decisions box-sizing: border-box appearance: none on controls body margin removal the typographic scale font: inherit on form controls link colour and underline style img display: block focus-ring colour and offset a consumer would accept all of these a consumer might reasonably decline these The test: would another team adopting your reset be surprised by this rule? If yes, it is base, not reset.

Pattern 1 — a reset layer that corrects, rather than flattens

Once you know the origin ordering, a reset stops being a specificity fight and becomes a short list of deliberate corrections.

@layer reset {
  /* WHY: no !important anywhere. Every one of these beats the UA default
     purely because it is an author declaration. */
  *, *::before, *::after { box-sizing: border-box; }
  body { margin: 0; }

  /* WHY: the UA sets a font on form controls that does NOT inherit. This is
     the single most valuable line in any reset. */
  button, input, select, textarea { font: inherit; color: inherit; }

  /* WHY: keep the UA's semantic defaults we actually want — list markers on
     real lists, the focus ring on keyboard navigation — by simply not
     touching them. A reset that removes outlines is an accessibility bug. */
}

Pattern 2 — normalising native form controls in base

Form controls are where UA styling is heaviest, and where a layered architecture pays off most clearly, because the corrections live in one predictable place instead of scattered through component files.

@layer base {
  /* WHY: `appearance: none` removes the platform widget rendering; without
     re-adding the affordances the control becomes unusable, so each removal
     is paired with a replacement. */
  input[type="checkbox"] {
    appearance: none;
    width: 1rem;
    height: 1rem;
    border: 2px solid var(--color-border);
    border-radius: var(--radius-sm);
    display: inline-grid;
    place-content: center;
  }

  /* WHY: the UA's :checked rendering disappeared with appearance:none, so the
     mark is drawn here rather than in a component layer — every checkbox in
     the product needs it, not just one component. */
  input[type="checkbox"]:checked::before {
    content: "";
    width: 0.55rem;
    height: 0.55rem;
    box-shadow: inset 1rem 1rem var(--color-primary);
    clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
  }
}

Pattern 3 — staying legible in forced-colors mode

Windows High Contrast (and any forced-colors: active environment) substitutes the user’s system palette for author colours after the cascade has resolved. Layers do not protect you, and they do not need to — the correct response is to stop fighting it.

@media (forced-colors: active) {
  @layer theme {
    /* WHY: system colour keywords are the only values that survive the
       substitution intact, so borders drawn with them stay visible when the
       author palette is discarded. */
    .card { border: 1px solid CanvasText; }

    /* WHY: backgrounds painted as decoration vanish in forced colors. An
       explicit forced-color-adjust: none is occasionally right for brand
       marks and charts — never for body text. */
    .brand-swatch { forced-color-adjust: none; }
  }
}

Interaction with adjacent features

Inline style attributes are author-origin declarations that sit outside the layer system entirely — like unlayered author styles, they outrank every layer. A framework that writes inline styles at runtime therefore cannot be controlled by layer order alone.

Animations and transitions occupy their own positions in the cascade sort, above all normal declarations. A running transition beats your utilities layer for the duration of the animation; this is expected and is not a layer-ordering bug.

Third-party CSS enters the author origin as soon as you load it, which is exactly why wrapping it in a layer works at all — see resolving third-party CSS conflicts.

!important inverts the ladder in both directions at once: across origins and across your own layers. The role of !important in layers covers the author-side half in depth.

DevTools diagnostic workflow

  1. Open DevTools → Settings → Preferences → Elements and enable Show user agent styles. UA declarations then appear in the Styles panel labelled user agent stylesheet.
  2. Select the element and read the winning rule for the property. The label tells you the origin: a layer annotation means author, user agent stylesheet means UA.
  3. If a UA rule is winning, check whether any author rule matches at all — filter the Styles panel by property name and look for a struck-through author declaration. No author declaration means the selector does not match, which is a selector bug, not a cascade bug.
  4. To test a suspected user-origin conflict, disable browser extensions and re-check. Extension-injected stylesheets are frequently author-origin (injected into the page) rather than true user styles, which changes the ordering.
  5. Emulate accessibility environments: Rendering → Emulate CSS media feature forced-colors → active, then re-inspect. Anything that disappears was relying on an author colour that the system palette replaced.
  6. Confirm no author !important remains whose only justification was “to beat the browser”:
# Every !important in the codebase, with file and line — review each one
# against the origin ladder before keeping it.
grep -rn "!important" src/styles --include="*.css"

Migration checklist

  1. Inventory the defensive important declarations. Any !important added to override a browser default can be deleted outright; author beats UA without it.
  2. Collect UA corrections into reset. Move box-sizing, margin: 0, font: inherit on controls, and image display: block out of component files and into the lowest layer.
  3. Separate correction from opinion. A rule that fixes an inconsistent browser default belongs in reset; a rule that expresses house style belongs in base or theme.
  4. Keep accessibility defaults. Do not remove focus rings, list semantics or :focus-visible behaviour in the name of normalisation.
  5. Audit inline styles. List every place the application writes element.style; those declarations bypass your layers and need a different fix.
  6. Run the forced-colors pass. Emulate forced-colors: active on your three most complex pages and fix anything illegible.
  7. Re-test with a user stylesheet installed if your audience includes readers who use one — the important user origin should be able to override your palette, and if it cannot, you have forced-color-adjust: none somewhere it does not belong.

Edge cases and gotchas

UA !important declarations you cannot beat

A small number of user-agent rules are marked !important, and no author declaration in any layer can override them. Historically this covered things like input[type=hidden] { display: none !important } in some engines. When a property refuses to change no matter what you write, check the UA rule for an importance flag before assuming a layer problem.

“My reset layer is too weak” is always a selector bug

Because the origin comparison precedes layer order, a reset declaration that fails to apply has lost to another author declaration, not to the browser. Search the author origin for the real competitor: an unlayered rule, an inline style, or a higher layer.

User styles are not the same as extension styles

Genuine user-origin stylesheets are rare — mostly per-site custom CSS features. Most extensions inject <style> elements into the page, which makes their rules author-origin and therefore subject to your layer order (usually as unlayered styles, so they win). The distinction matters when you are trying to reproduce a reader’s bug report.

forced-color-adjust: none disables an accessibility guarantee

It is the correct tool for a brand logo or a data visualisation whose meaning is carried by colour. Applying it to text, form controls or focus indicators removes the reader’s ability to make your interface legible, and no cascade layer restores it.

The important inversion applies inside your layers too

Adding !important does not simply promote a declaration — it moves it into a competition where your own layer order runs backwards. A team that sprinkles !important through utilities to “make utilities win” discovers that reset important declarations now beat them. Fix the layer order instead; preventing style collisions in large frontend teams covers the governance side.

FAQ

Can a low-priority cascade layer lose to the user-agent stylesheet?

No. Origin and importance are compared before cascade layer, and every normal author declaration outranks every normal user-agent declaration. A rule in your lowest reset layer overrides the browser default with no !important and no specificity games. When a UA style appears to win, the author rule is not matching the element — inspect the selector rather than the layer order.

Do cascade layers apply to user-agent and user stylesheets?

Each origin sorts its own declarations, and layers are part of that per-origin sort. Your author layers order author declarations only; they have no effect on the UA stylesheet and cannot promote an author rule into the user origin. A user stylesheet is free to declare its own layers, and those order only that stylesheet’s rules.

Why does an !important user style beat my !important author style?

Importance reverses the origin ladder. Normal declarations run user agent → user → author, so authors normally win. Important declarations run author → user → user agent, so the reader wins. This is deliberate: it guarantees a reader who needs larger text or higher contrast can always get it, regardless of what the site’s CSS says.

Does forced-colors mode ignore my cascade layers?

It does not touch layer resolution. Your layers still decide which declaration wins; forced colors then replaces the resulting colour values with the user’s system palette for a defined set of properties. Design for it rather than around it: use system colour keywords for borders that must stay visible, and reserve forced-color-adjust: none for brand marks and charts.

Guides in This Section

Up: Specificity Management & Conflict Resolution