---
title: Cascade Layers
description: CSS cascade layers refer to the order in which CSS rules are applied to an HTML element.
---

When multiple CSS rules apply to the same element, the browser uses the cascade to determine which rule should take
precedence. See the [MDN article](https://developer.mozilla.org/en-US/docs/Web/CSS/@layer) to learn more.

Bamboo takes advantage of the cascade to provide a more efficient and flexible way to organize styles. This allows you
to define styles in a modular way, using CSS rules that are scoped to specific components or elements.

## Layer Types

Bamboo's Vite compiler emits four cascade layers:

- `@layer reset` - The reset layer is used to reset the default styles of HTML elements. This is used when
  `preflight: true` is set in the config. You can also use this layer to add your own reset styles.

The generated CSS for the reset layer looks like this:

```css
@layer reset {
  * {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
  }
  /* ... */
}
```

- `@layer base` - The base layer contains global styles defined in the `globalStyles` key in the config. You can also
  use this layer to add your own global styles.

The generated CSS for the base layer looks like this:

```css
@layer base {
  a {
    color: #000;
    text-decoration: none;
  }
  /* ... */
}
```

Recipe declarations are resolved before CSS is materialized. Their declarations use the same global atoms as `css()` and
therefore live in `@layer utilities`; no named recipe layer is emitted by Vite. When analyzable styles are composed, the
compiler resolves later-wins precedence before allocating those atoms.

- `@layer tokens` - The tokens layer contains css variables for tokens and semantic tokens. You can also use this layer
  to add your own design tokens.

The generated CSS for the tokens layer looks like this:

```css
@layer tokens {
  :root {
    --color-primary: #000;
    --color-secondary: #fff;
    --color-tertiary: #ccc;
    --shadow-sm: 0 0 0 1px rgba(0, 0, 0, 0.05);
  }
  /* ... */
}
```

- `@layer utilities` - Styles that are scoped to a specific utility class. These styles are only applied to elements
  that have the utility class applied.

## Layer Order

The cascade layers are applied in the following order:

- `@layer utilities` (Highest priority)
- `@layer tokens`
- `@layer base`
- `@layer reset` (Lowest priority)

Recipe and utility styles share this layer under Vite. Use `cx(recipe(), css())` or compose style objects in the desired
order so the compiler can remove overridden declarations before CSS is emitted.

## Inside a layer, specificity still decides

Layers decide between rules in **different** layers. Within one layer nothing has changed about CSS: specificity
decides, and source order breaks the tie. Two `css()` calls are always in the same layer, so one can outrank the other —
and nesting is what makes their specificity unequal.

```tsx
const prose = css({
  '& p': { fontSize: '14.5px' }, //  (0,1,1) — a class and an element
})

const lede = css({ fontSize: '16px' }) //  (0,1,0) — a class

<article className={prose}>
  <p className={lede}>This renders at 14.5px.</p>
</article>
```

The paragraph carries `lede` directly and still loses, because `& p` is the more specific selector. Neither call knows
about the other, and nothing in the authoring experience suggests one `css()` can outrank another — the CSS is correct,
the class is on the element, and the value is simply not the one applied.

A nested selector reaches further than it looks, too. `'& a': { textDecorationLine: 'underline' }` on an article
underlines every anchor inside it, including ones in a navigation card that styles itself.

Scoping bounds what a selector can reach, and is worth doing at the point you reach for nesting rather than after a
screenshot diff:

```tsx
css({
  '& > p': { fontSize: '14.5px' }, // direct children, not paragraphs anywhere below
  '& :is(p, li) a': { textDecorationLine: 'underline' }, // anchors in body text, not every anchor in the subtree
})
```

It does not draw a component boundary, though — a card nested in that article that renders a `<p>` is still inside
`& :is(p, li) a` — and it does not change the arithmetic: `& > p` is `(0,1,1)` too, so it still outranks a class on the
paragraph it matches.

What removes the question is putting the styles on the element itself. That is the
[recommendation for nested styles](/docs/concepts/writing-styles#native-css-nesting) generally, and this is why.

`@bamboocss/eslint-plugin` reports nested selectors that reach another element — all of them, scoped or not, since all
of them outrank a class on what they match — for projects that want this enforced rather than remembered:

```js filename="eslint.config.js"
rules: {
  'bamboo/no-descendant-selectors': 'warn',
}
```

## Layer CSS

The first line of the generated CSS declares the layer order:

```css
@layer reset, base, tokens, utilities;
```

Extraction-only CLI and PostCSS output retains the named `recipes` layer because it has no source compiler to resolve
recipe selections into shared atoms.

## Customize layers

Bamboo lets you customize the cascade layers, so your project can coexist with other solutions. Learn more about
customizing layers [here](/docs/references/config#layers).

## Polyfills

The Vite compiler rejects cascade-layer polyfills: flattening layers destroys the boundary its reachability and atom
renaming pass relies on. For extraction-only PostCSS output, you can use:

- [postcss-cascade-layers](https://www.npmjs.com/package/@csstools/postcss-cascade-layers): Adds support for CSS Cascade
  Layers.

Here is an example of a `postcss.config.js` file that uses these polyfills:

```js
module.exports = {
  plugins: ['@bamboocss/dev/postcss', '@csstools/postcss-cascade-layers'],
}
```

Since CSS `@layer`s have a lower priority than other CSS rules, this postcss plugin is also useful in cases where your
styles are being overridden by some other stylesheets that you're not in total control of, since it will remove the
`@layer` rules and still emulate their specificity.
