---
name: cascade-layers
description: "Audits and fixes CSS that loses to other CSS: specificity wars, !important chains, vendor or Tailwind styles that win, and cascade layer order. Runs a read-only checker for unlayered imports, late layer order statements, !important inside layers, id selectors and high specificity, plus a resolver that ranks which declaration wins, then plans a layer migration in approved batches. Use for CSS not applying, @layer, specificity, overriding third-party styles. Do NOT use for layout overflow, container queries, CSS-in-JS runtime bugs or class naming schemes."
license: CC-BY-4.0
metadata:
  author: Afeez Adebayo
  author_url: https://afeezadebayo.com
  author_linkedin: https://www.linkedin.com/in/afeez-adebayo/
  author_x: https://x.com/za_icon
  source: https://afeezadebayo.com/skills-md/frontend-development/css-layout/cascade-layers/
  category: frontend-development
  subcategory: css-layout
  version: "1.0.0"
  created: "2026-10-25"
  updated: "2026-10-25"
---

# Cascade Layers and Specificity: Why a Rule Loses and How to Fix It

An agent fixes CSS conflicts in this order: run a read-only checker over the stylesheets in load order, find which declaration actually wins for the failing element with a resolver, decide whether cascade layers are worth adopting, then migrate in small approved batches and confirm each result in a browser.

It works on plain CSS, bundler output and `<style>` blocks. Facts and measurements sit in `references/appendix.md`, migration code in `references/migration.md`.

## When to use this skill, and when not to

Use it when a rule does not apply, when a vendor or framework style beats yours, when `!important` is spreading, or when you want to set up or repair `@layer`.

| Request | Use instead |
|---|---|
| Overflow, shrinking, grid tracks | `flexbox-grid-layout.md` |
| Component width responding to its container | `container-queries.md` |
| `@scope`, CSS Modules, shadow DOM design | Out of scope for this release |
| CSS-in-JS class order at runtime | The library's own docs. This skill reads static CSS |
| Class naming schemes such as BEM | Out of scope |

## Guardrails

1. **Propose, then change.** Report and plan first. Edit only after the user approves a batch, on a branch under version control. Never push, merge or deploy.
2. **Never edit vendor files.** Do not change files in `node_modules`, vendor folders or CDN copies. Wrap them in a layer at the import instead.
3. **No credentials in output.** Stylesheets can hold signed `url()` values or tokens in query strings. The checker truncates values to 30 characters, the browser snippet to 70. Do not paste `url(...)` values into reports.
4. **Batches stay small.** At most 1 file or 20 rules per batch (defaults), each shown as a diff.
5. **Nothing is invented.** Thresholds are labelled defaults. Source facts carry a date. Version-dependent facts carry **verify before relying on this**.
6. **Layers are not a fix for a small project.** Adopt them only past the thresholds in step 3.
7. **Refuses to:** add `!important`, win with a style attribute, add an id selector, reorder layers by guesswork, or claim a result without the browser check in step 7.

## Inputs the agent must have before starting

Ask for every missing item. Do not assume.

| Input | Ask exactly |
|---|---|
| Stylesheets in load order | "List every stylesheet in the order the page loads it, including `@import`s, framework CSS and any CDN files." |
| The failing case | "Which element, which property, what value do you expect and what value do you get?" |
| Third-party CSS | "Which stylesheets can you not edit?" |
| Framework | "Do you use Tailwind, Bootstrap or another framework that already declares layers? Which version?" |
| Browsers | "Which browsers must work? Layers are Baseline widely available (MDN, March 2022)." |
| How to run the page | "How do I open it locally?" |
| Approval | "May I edit on a branch in batches, showing each diff first?" |

## Workflow

### Step 0: Confirm scope

List the files in load order. Minified input is fine for the checker, but fixes go in source files. One failing case at a time.

### Step 1: Run the checker

```
python3 scripts/cascade_check.py vendor.css app.css | head -40
```

Read only its output. Do not read the script. Pass files in load order. `--max-spec 3` changes the class-level threshold. Exit code 1 means an error. The last two lines give the layer order and counts. Each code has one fix, in this table.

| Code | Level | Meaning | Fix |
|---|---|---|---|
| `import-no-layer` | warn | An `@import` without `layer()` is unlayered, so it beats every layered rule | `@import url(x.css) layer(vendor);` |
| `import-after-rules` | error | `@import` after a style rule is invalid and ignored | Move it above the rules, after any `@layer` statement |
| `no-order-statement` | warn | Layer blocks appear with no earlier `@layer a, b;`, so order follows first appearance | Add one statement at the top of the first stylesheet loaded |
| `order-statement-late` | warn | A statement names layers already ordered; it cannot move them | Delete it and put the full order in the first statement |
| `utilities-not-last` | warn | A later layer beats utilities | Move `utilities` to the end of the order, before any `*important*` layer |
| `important-in-layer` | warn | Important order is reversed: an earlier layer's `!important` beats a later one | Step 6 |
| `anonymous-layer` | info | An unnamed layer cannot be extended or reordered by name | Name it |
| `id-selector` | warn | An id selector, specificity (1,0,0) or more | Class selector |
| `high-specificity` | warn | Class-level specificity of 3 or more | Flatten the selector, or place it in a later layer |

### Step 2: Find what wins for the failing case

1. In the browser, run the matched-rules snippet (Templates) on the element. It lists each matching rule with its layer. Cross-origin stylesheets are skipped by the browser, so read those in DevTools.
2. Run the resolver with the selectors from that list:

```
python3 scripts/cascade_check.py vendor.css app.css --rank background --sel ".btn" --sel ".bg-brand"
```

The first line starts `WINS` and shows layer, specificity and importance. It models layers, importance, specificity and source order. It does not model style attributes, animations or selectors outside the files you passed.
3. Confirm with `getComputedStyle(el).background` in the browser. If the resolver and the browser disagree, a style attribute, an unscanned file or a transition is involved. Stop and find it before editing.

### Step 3: Decide whether to adopt layers

Read the counts line from step 1. Defaults, adjust with the user:

| Finding | Decision |
|---|---|
| Third-party CSS you must override, or `!important` count of 10 or more outside a utilities layer, or 5 or more selectors flagged `high-specificity` or `id-selector` | Adopt layers: steps 4 to 7 |
| Fewer than that, and the failing case is one or two rules | Fix locally: lower the selector's specificity, wrap resets in `:where()`, no new layers |
| A framework already declares layers | Insert yours around its names. Do not redeclare its names in another order |

A small single-file project with no conflicts gains structure cost and little else (CODERCOPS, June 2026, one practitioner source).

### Step 4: Design the order

Declare one statement, as the first line of the first stylesheet loaded:

```css
@layer reset, vendor, base, components, utilities, legacy-important;
```

| Rule | Reason |
|---|---|
| Later layers beat earlier ones whatever the specificity | Spec behaviour (MDN) |
| Unlayered CSS beats every layer for normal declarations | Your unlayered code is the final override. A layer can never beat it (csswg-drafts #6284: kept as is, "already widely used") |
| Order comes from first appearance, so the statement goes first | A later statement cannot reorder (Chrome, 2022) |
| Third-party CSS goes in `vendor`, or `thirdparty.name` when there are several | Your unlayered overrides then win without specificity tricks (OddBird, CODERCOPS). csswg-drafts #6323 gives the same advice |
| `utilities` last among normal layers | A component rule in a later layer would beat a utility |
| `legacy-important` last | `!important` order is reversed, so the last layer's important rules are the weakest of the layered ones and still beat every normal declaration |
| Name every layer | Anonymous layers cannot be extended |

A stylesheet loaded with `<link>` cannot be assigned a layer (Chrome, 2022, open WHATWG issue; **verify before relying on this**). Import it from CSS with `layer()`, or wrap it at build time. If the first CSS is a `<link>`, put the order statement in an inline `<style>` before it.

### Step 5: Migrate in batches

In this order, one batch per bullet, checker and step 2 after each:

1. Add the order statement.
2. Wrap vendor CSS: `@import url(vendor.css) layer(vendor);` after the statement.
3. Move the reset into `reset`.
4. Leave everything else unlayered. It already wins over layers, so nothing changes yet.
5. Move one stylesheet at a time into `base`, `components` or `utilities`. After each, rank the previously failing elements and compare with the browser.
6. Stop when the failing cases pass. Moving more is optional.

A migration can expose rules that only won by source order (CODERCOPS, June 2026). Re-rank the five most specific selectors in each moved file.

### Step 6: Handle `!important`

| Case | Action |
|---|---|
| Used only to beat a selector that is now in an earlier layer | Delete it |
| Used on a utility class meant to always win | Delete it if `utilities` is the last layer. Layers now do that job |
| Vendor or legacy rule you cannot yet remove | Move it to `legacy-important` so all legacy importants keep their relative specificity order |
| Needed to beat inline styles | Leave it and record the reason. An `!important` rule beats a normal style attribute |

Moving an `!important` rule into a low layer makes it stronger, not weaker, against important rules in later layers (OddBird, 2021 explainer).

### Step 7: Verify

1. Run the checker again. Errors must be 0, `no-order-statement`, `order-statement-late` and `import-no-layer` must be gone, and each remaining warning is fixed or listed with a reason.
2. For every failing case from step 2, run the resolver and `getComputedStyle`. They must show the expected value and agree with each other.
3. Open the two most visited pages and compare before and after screenshots, using fake data.
4. Run the existing test suite if there is one.
5. List what you could not test: browsers older than your target, pages behind a login.

### Step 8: Report

Use the report template. A short list of file, line, issue and fix.

## Worked example

An invented site: `vendor.css` (a button library) is imported without a layer, `app.css` declares `components` and `utilities` blocks, puts the `@layer base, components, utilities;` statement after them, sets two `!important` declarations inside layers, and uses an id selector. The page has `<a class="btn bg-brand">`, and the orange `bg-brand` background never shows.

Step 1 (9 warnings and 0 errors, six shown):

```
app.css:1  warn  import-no-layer  @import url("vendor.css") is unlayered: it beats every layered rule. Use layer(name)
app.css:2  warn  no-order-statement  layer blocks appear with no earlier `@layer a, b, c;` statement ...
app.css:5  warn  order-statement-late  @layer components was already ordered at app.css:2; this statement cannot move it
app.css:4  warn  important-in-layer  background: !important in layer components: important order is reversed ...
app.css:5  warn  utilities-not-last  layer 'utilities' is not last in the order ['components', 'base', 'utilities', 'reset']
app.css:11  warn  id-selector  #hero .btn.primary.large has an id selector, specificity (1, 3, 0)
```

Step 2:

```
WINS  vendor.css:1  .btn  layer=unlayered  spec=(0, 1, 0)  important=False  background: gray
      app.css:8  .bg-brand  layer=utilities  spec=(0, 1, 0)  important=False  background: orangered
```

The browser agrees: `getComputedStyle` gives `rgb(128, 128, 128)`. Step 3: a third-party stylesheet that must be overridden, so adopt layers.

Fixes in one batch (`app.css` only): the order statement first, the import with `layer(vendor)`, the reset in `reset`, `!important` removed from the utility and the legacy rule moved out of the way, the id selector replaced by a class. Step 7: checker `errors 0, warnings 0`, and `getComputedStyle` gives `rgb(255, 69, 0)`. The resolver named the same winner as Chromium 153 in all 18 cases of the skill's own test set, measured on 25 October 2026.

## Templates

### Matched rules in the browser (step 2)

```js
(sel) => { const el = document.querySelector(sel), out = [];
  const walk = (rules, layer) => { for (const r of rules) {
    if (r.constructor.name === 'CSSLayerBlockRule') walk(r.cssRules, layer ? layer + '.' + r.name : r.name);
    else if (r.constructor.name === 'CSSImportRule') { try { walk(r.styleSheet.cssRules, r.layerName === null ? layer : (layer ? layer + '.' : '') + r.layerName); } catch (e) {} }
    else if (r.cssRules && r.selectorText === undefined) walk(r.cssRules, layer);
    else if (r.selectorText) { let m = false; try { m = el.matches(r.selectorText); } catch (e) {} if (m) out.push((layer || 'unlayered') + ' | ' + r.selectorText + ' | ' + r.style.cssText.slice(0, 70)); } } };
  for (const s of document.styleSheets) { try { walk(s.cssRules, ''); } catch (e) {} }
  return out; }
```

Call it with a selector for the element, for example `('a.btn')`. It prints CSS only, never page text.

### Computed value (steps 2 and 7)

```js
getComputedStyle(document.querySelector('a.btn')).getPropertyValue('background-color')
```

### Report

```
Cascade report: <page>, <date>
Failing case: <element, property, expected, got>. Cause: <layer, specificity, importance of the winner>.
Checker before: errors N, warnings N. After: errors N, warnings N.
Layer order: <statement>.
Changes: <file:line  issue  fix>   (one line each)
Left as is: <warning  reason>
Not tested: <items>
```

### Starting prompt

```
Use the cascade-layers skill. Stylesheets in load order: <list>. Failing case: <element, property, expected, got>.
Third-party files I cannot edit: <list>. Framework: <name and version>. Edit on a branch in batches of one file or 20 rules and show each diff first.
```

## Failure modes and anti-patterns

| Mistake | How to spot it | Do instead |
|---|---|---|
| Expecting a later layer to beat unlayered CSS | A utility in `utilities` does nothing, and the resolver shows an unlayered rule winning | Put the vendor or legacy CSS in a layer, or move the unlayered rule into a layer |
| Treating `!important` as "wins everywhere" inside layers | An important utility is ignored by an important rule in an earlier layer | Step 6: delete it, or use `legacy-important` last |
| Letting first appearance set the order | `no-order-statement` or `order-statement-late` | One statement, first line of the first stylesheet |
| Layering a vendor sheet by editing it, or via `<link>` | A diff inside `node_modules`, or a `<link>` the browser leaves unlayered | `@import ... layer()` from your own CSS |
| Treating layers as scoping or as z-index numbers | A rule in `components` styles elements everywhere, and someone adds layers to fix one conflict | Scope selectors as before. Add a layer only past the step 3 thresholds |

## Quality bar

Run every item before handing over.

1. Checker errors are 0, and `no-order-statement`, `order-statement-late` and `import-no-layer` no longer appear.
2. Each failing case from step 2 shows the expected value in `getComputedStyle`, and the resolver names the same winner.
3. The order statement is one line, first in the first stylesheet loaded, and every layer is named.
4. No `!important` was added, and each remaining one is listed with a reason.
5. No file under `node_modules` or a vendor folder was edited.
6. Every changed line appears in the report with file, line, issue and fix.
7. The report lists what was not tested.
8. No `url(...)` value with a query string appears in the output.

## Cost

Figures are estimates from this skill's own count (words × 1.35 for prose, characters ÷ 3.5 for code and tables), not from a model tokeniser. Rival figures are estimates from the excerpts I read.

| Item | Tokens | Status | Read condition |
|---|---|---|---|
| A. Description (always loaded) | 112 | Estimate | Every session with the skill installed |
| B. `SKILL.md` body | 4,634 | Estimate | When the skill fires |
| C. `references/appendix.md` | 1,201 | Estimate | Only to check a claim or a measurement |
| C. `references/migration.md` | 510 | Estimate | Only when writing the layer statement, imports or wrappers |
| Script output: `cascade_check.py` (worked example) plus measuring snippets | 680 | Estimate | Every run. Checker output is capped with `head -40`, about 1,382 tokens |
| D. Typical run (A + B + script and snippet output) | 5,426 | Estimate | Common path: fix flagged lines, no references |
| D. Worst case (A + B + both references + 40 lines of output) | 8,439 | Estimate | Rebuilding a layout, many findings |
| E. Cheaper path | 5,201 | Estimate | Run the checker, fix only flagged lines, skip the references and the browser snippet |

Rival sizes, estimated from the excerpts I read (SKILL.md only, files they link were not sized): thedaviddias cascade-layers (tessl) about 300 words, roughly 405 tokens, no script or verification step, tessl quality score 61%; jamiojala skillforge cascade-layers-master (openskillindex) about 450 words, roughly 610 tokens, names hooks it never defines; openaec frontend-syntax-css-cascade-layers-scope (skills.sh) at least 110 visible words, full size unknown, covers `@scope` gating. This skill costs more to load than all three. What the checker saves is the agent reading and reasoning over every rule in a large stylesheet. Measured on `bootstrap.min.css` (235,601 characters) the checker printed 1,862 characters in 0.57 seconds. On the two-file worked example its output is larger than the files, so on a stylesheet under about 100 lines read the file and use the resolver.

## Handoffs

`flexbox-grid-layout.md` covers overflow and shrinking. `container-queries.md` covers components that respond to their container.

## Appendix

Tool facts, measurements and sources are in `references/appendix.md`. Migration code is in `references/migration.md`.

## Sources

Last verified 25 October 2026 for every source, except where a date says otherwise.

- Anthropic, Skill authoring best practices: https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices
- MDN, @layer: https://developer.mozilla.org/en-US/docs/Web/CSS/@layer
- Chrome for Developers, Cascade layers are coming to your browser (1 February 2022): https://developer.chrome.com/blog/cascade-layers
- OddBird, Cascade Layers explainer (2021): https://css.oddbird.net/layers/explainer/
- csswg-drafts issue 6284: https://github.com/w3c/csswg-drafts/issues/6284
- csswg-drafts issue 6323 (archived July 2024): https://lists.w3.org/Archives/Public/public-css-archive/2024Jul/0063.html
- CODERCOPS, CSS Cascade Layers: Production Notes (8 June 2026): https://blog.codercops.com/blog/css-cascade-layers-production-2026/
- thedaviddias, cascade-layers (via tessl). Rival implementation: https://tessl.io/registry/thedaviddias
- jamiojala, cascade-layers-master (via openskillindex). Rival implementation: https://openskillindex.com
- openaec, frontend-syntax-css-cascade-layers-scope (via skills.sh). Rival implementation: https://skills.sh

---
Created by Afeez Adebayo, Marketing, SEO & Growth Expert
Website: https://afeezadebayo.com
LinkedIn: https://www.linkedin.com/in/afeez-adebayo/
X: https://x.com/za_icon
Source: https://afeezadebayo.com/skills-md/frontend-development/css-layout/cascade-layers/

Licence: CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/). You may copy, share and adapt this skill, including for commercial use, as long as you credit Afeez Adebayo and link to the source URL above. Keep this notice in any copy or adapted version.
