---
title: "CSS: Cascade Layers"
slug: "css-cascade-layers"
category: "frontend"
tags: ["frontend", "css", "cascade-layers", "specificity"]
status: "stable"
last_updated: 2026-05-14
summary: "@layer for predictable cascade order: reset, base, components, utilities, overrides. Tame third-party CSS, retire !important, and debug in DevTools."
related:
  [
    "[[frontend/css]]",
    "[[frontend/css-custom-properties]]",
    "[[frontend/tailwind]]",
    "[[frontend/shadcn]]",
    "[[frontend/accessibility]]",
  ]
---

## Overview

`@layer` lets you declare named buckets and an explicit precedence between them. Selector specificity is resolved inside a layer; the layer order resolves between layers. The result is a cascade you can read off the top of the file instead of debugging with DevTools. The umbrella for CSS architecture is [[frontend/css]].

## Declare layer order once, at the top

A single `@layer` declaration sets the order. Later layers win, regardless of specificity inside them.

```css
@layer reset, base, components, utilities, overrides;

@layer reset {
  /* normalize, box-sizing */
}
@layer base {
  :root {
    --color-ink: oklch(0.2 0 0);
  }
  body {
    font-family: system-ui;
  }
}
@layer components {
  .card {
    padding: var(--space-3);
    border-radius: var(--radius-card);
  }
}
@layer utilities {
  .text-center {
    text-align: center;
  }
}
@layer overrides {
  .force-block {
    display: block;
  }
}
```

Anything written outside any layer wins over everything inside layers. Use that lane for true emergency overrides only, and document why.

## Use the five-layer stack: reset, base, components, utilities, overrides

The names are a convention, but this set covers most apps.

- `reset`: normalize, box-sizing, focus-visible defaults.
- `base`: token definitions, element defaults (`body`, `h1`, `a`), [[frontend/css-custom-properties]] on `:root`.
- `components`: reusable component classes, `[data-variant]` modifiers.
- `utilities`: single-purpose utilities (Tailwind's output goes here; see [[frontend/tailwind]]).
- `overrides`: page-level fixes, scoped temporary patches.

A new layer is sometimes justified (a `vendor` lane for third-party CSS, a `themes` lane for skinning). Add it to the declaration line; do not let order drift.

## Quarantine third-party CSS in its own layer

Most cascade pain comes from a vendor stylesheet whose specificity outranks yours. Wrap it in a layer that sits below your code.

```css
@layer vendor, reset, base, components, utilities;

@import url("/static/vendor/calendar.css") layer(vendor);
```

Now your `.calendar-day { color: var(--color-ink); }` in `components` wins over the vendor's `.calendar .day { color: #333; }`, without `!important` and without selector escalation.

## Retire `!important` from app code

`!important` skips the layer order. A single `!important` in `components` outranks every rule in `utilities`. That defeats the point.

- App rules: never use `!important`. If you need to win, move the rule to a later layer.
- Utility classes inside `@layer utilities`: do not stamp `!important`. The layer order already wins.
- Library or print stylesheets that must defeat host CSS: justified, isolated to one file.

If `!important` is the only fix, the underlying problem is a layer-ordering bug. Find it and fix the order instead.

## Layer-aware specificity inside a layer

Inside a single layer, normal specificity applies: a class beats an element, an ID beats a class. Across layers, specificity is irrelevant; the layer order wins. Two rules to remember.

- Specificity is intra-layer.
- Order is inter-layer.

A `#sidebar a` rule in `base` loses to a `.link` rule in `utilities`. The id wins inside `base`; the layer order wins across.

## Debug layers in DevTools

Chrome, Firefox, and Safari all show layer assignments in the Styles panel.

- Each rule lists its layer name next to the selector.
- The cascade column shows which rules are overridden and why (layer, specificity, source order).
- Filter by `@layer` to see only the rules that won for an element.

If a rule shows up grey with another layer's rule winning, the fix is to move the rule to a later layer, not to escalate the selector.

## Order Tailwind and shadcn carefully

Tailwind v4 emits its utilities into `@layer utilities` and its base reset into `@layer base`. [[frontend/shadcn]] components ship as raw CSS or as classes that expect to live in `@layer components`. Set the order explicitly so utilities can override components.

```css
@import "tailwindcss";

@layer reset, base, components, utilities;
```

Without an explicit declaration, the order falls to import order, and a stray third-party stylesheet imported late can outrank your utilities.

## Common pitfalls

- **A rule outside any layer wins over a rule inside one.** Forgetting to wrap an `@import` is the usual cause. Add `layer(name)` to the import.
- **Unnamed `@layer { }` blocks.** Anonymous layers join the declaration order in source order. Use named layers so the order line is the source of truth.
- **Animations and `@keyframes` inside a layer.** The animation name resolves across layers as normal cascading; the `@keyframes` rule itself is not layered. Define keyframes once, outside layers or in `base`.
- **Mixing legacy `!important` with layers.** A legacy `!important` rule inverts the layer order for that property. Audit and remove.
- **Authoring tokens in `components`.** Tokens belong in `base` so every later layer can read them; see [[frontend/css-custom-properties]].

## Related

- [[frontend/css]]
- [[frontend/css-custom-properties]]
- [[frontend/tailwind]]
- [[frontend/shadcn]]
- [[frontend/accessibility]]
- [[seo/page-speed]]
