CSS Nesting and Cascade Layers: Organise Large Stylesheets
Write cleaner CSS with native nesting and the & selector, then control which styles win with @layer: order, unlayered styles, !important and third-party CSS.
What you will learn
As a stylesheet grows, two problems appear. First, related rules get scattered: the card’s title, text, buttons and hover states live in five different places, all repeating .card. Second, specificity wars break out: you add !important or longer and longer selectors just to make your own rule beat a library’s. Two modern CSS features solve exactly these problems. Native CSS nesting lets you write related rules inside each other. Cascade layers (@layer) let you decide, explicitly, which group of styles wins. In this lesson you will learn both, and how to combine them into a clean architecture.
The HTML we will style
<h1 class="page-title">Nesting and Layers Lab</h1>
<!-- Part 1: nesting. One .card rule holds everything about the card -->
<article class="card">
<h2 class="card-title">Nested card</h2>
<p class="card-text">Hover the card, then hover the links and buttons inside it.</p>
<p class="card-actions">
<a href="#more" class="card-link">Read more</a>
<button class="btn" type="button">Save</button>
<button class="btn btn-danger" type="button">Delete</button>
</p>
</article>
<!-- Part 2: cascade layers -->
<section class="layer-demo">
<p class="message">A plain message.</p>
<p class="message urgent">An urgent message.</p>
</section>
| Selector | Element | Role |
|---|---|---|
.card |
the <article> |
the parent rule that holds the nested rules |
.card-title, .card-text |
the <h2> and the first <p> |
children styled inside .card { ... } |
.card-actions |
the <p> that holds the actions |
wrapper for the link and two buttons |
.card-link |
the <a> |
a link with a nested :hover |
.btn, .btn-danger |
the two <button>s |
a base button, plus a variant |
.layer-demo |
the <section> |
wrapper for the layers experiment |
.message |
both <p>s |
the base message style |
.urgent |
the second <p> (it has both message and urgent) |
the style that should win |
Part 1: Native CSS nesting
The problem nesting solves
Without nesting, every rule about the card repeats the card’s selector:
.card { padding: 20px; }
.card:hover { box-shadow: 0 10px 24px rgba(0, 0, 0, 0.12); }
.card .card-title { margin: 0 0 8px; }
.card .card-text { color: #475569; }
.card .card-link:hover { text-decoration: underline; }
This is repetitive, and the rules about one component are not visibly grouped. If you rename .card, you edit every line.
Nested rules
With nesting, you write the child rules inside the parent rule:
.card {
padding: 20px;
&:hover {
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.12);
}
.card-title {
margin: 0 0 8px;
}
.card-text {
color: #475569;
}
.card-link:hover {
text-decoration: underline;
}
}
The browser reads this exactly like the long version above. Nesting is built into CSS itself, so no preprocessor such as Sass is needed. It works in all current browsers.
The & selector
The ampersand & stands for the parent selector. You use it when you want to attach something to the parent itself rather than to its descendants:
.card {
/* &:hover → .card:hover (the card itself, hovered) */
&:hover {
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.12);
}
/* &.featured → .card.featured (a card that also has the class "featured") */
&.featured {
border-color: royalblue;
}
/* & + & → .card + .card (a card that follows a card) */
& + & {
margin-top: 16px;
}
/* .dark & → .dark .card (a card inside .dark: the parent goes at the END) */
.dark & {
background-color: #0f172a;
}
}
Written inside .card { } |
Means | Why |
|---|---|---|
.card-title { } |
.card .card-title |
a descendant (no & needed) |
& .card-title { } |
.card .card-title |
the same, written explicitly |
> p { } |
.card > p |
a direct child (a combinator at the start) |
&:hover { } |
.card:hover |
& is required to attach a pseudo-class to the parent |
&.featured { } |
.card.featured |
& is required to add a class to the parent |
.dark & { } |
.dark .card |
put & last to add a context around the parent |
The most common slip is leaving out the & when you mean the element itself. Compare:
.card {
:hover { color: red; } /* .card :hover → any hovered DESCENDANT of the card */
&:hover { color: red; } /* .card:hover → the card itself, hovered */
}
Nesting media queries and other at-rules
You can put @media, @container and @supports inside a rule, so a component’s responsive behaviour stays next to the component:
.card {
padding: 16px;
@media (min-width: 700px) {
padding: 28px;
}
}
No more hunting for the matching rule in a media query 400 lines away.
The nested card, fully
.card {
max-width: 420px;
padding: 20px;
margin-bottom: 32px;
border: 1px solid #e2e8f0;
border-radius: 12px;
transition: box-shadow 0.2s ease;
&:hover {
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.12);
}
.card-title {
margin: 0 0 8px;
color: #0f172a;
}
.card-text {
margin: 0 0 12px;
color: #475569;
}
.card-link {
color: royalblue;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
.btn {
padding: 8px 14px;
border: none;
border-radius: 6px;
background-color: royalblue;
color: white;
font: inherit;
cursor: pointer;
&:hover {
background-color: #1d4ed8;
}
&.btn-danger {
background-color: crimson;
&:hover {
background-color: #9f1239;
}
}
}
}
Notice that nesting goes three levels deep here (.card → .btn → &.btn-danger → &:hover).
Differences from Sass
If you have used Sass, remember these differences:
&-suffixdoes not work. In Sass,.card { &-title { } }creates.card-title. In native CSS, that does not work. Write the full class name (.card-title).- Nested rules can be written without
&for descendants, but pseudo-classes and compound classes need it. - Native nesting is live in the browser. There is nothing to compile.
Nesting and specificity
A nested rule behaves as if the parent selector were wrapped in :is(). So .card .card-title has the specificity of .card plus .card-title, which is 0-2-0, and .card:hover is 0-2-0. When the parent is a list (.a, .b { .c { } }), it counts as the most specific item in the list, like :is().
How deep should you nest?
Nesting makes it easy to build very specific selectors without noticing. A good rule: stay within three levels, and do not nest just to follow the HTML structure. If you find yourself writing .page { .main { .article { .card { .title { } } } } }, give the title its own class and write one flat rule instead.
Part 2: Cascade layers
The problem
You already know the order of the cascade: importance, then inline styles, then specificity, then source order. A library’s CSS might use .nav ul li a.active, and to override it you have to write something even more specific, or reach for !important. A cascade layer lets you put whole groups of styles in a priority order, so that a layer’s position matters more than the specificity of its selectors.
Creating layers
@layer reset, base, components, utilities;
This one line declares the order of the layers, from the weakest to the strongest. Here reset is the weakest and utilities is the strongest. Do it once at the top of your stylesheet. Then put styles into the layers:
@layer reset {
* {
box-sizing: border-box;
margin: 0;
}
}
@layer base {
body {
font-family: Arial, Helvetica, sans-serif;
}
p {
color: #475569;
}
}
@layer components {
.layer-demo .message {
padding: 12px 16px;
border-radius: 8px;
background-color: #eff6ff;
color: #1e40af;
}
}
@layer utilities {
.urgent {
background-color: #fef2f2;
color: #b91c1c;
font-weight: bold;
}
}
The key idea: later layers win, regardless of specificity
Look at the second message: <p class="message urgent">. Two rules apply to its colour:
- In
components:.layer-demo .messagescores 0-2-0. - In
utilities:.urgentscores only 0-1-0.
Without layers, the first would win (higher specificity). With layers, the utilities layer was declared after components, so .urgent wins, even though its selector is weaker. You can write low-specificity utility classes and be sure they will always beat component styles.
The priority is, from weakest to strongest:
reset < base < components < utilities < (styles outside any layer)
Styles outside any layer win over all layers
This is a rule that surprises people: unlayered styles beat every layered style, whatever the specificity:
@layer components {
.message { color: royalblue; }
}
/* NOT in a layer: this wins, even though it is weaker */
p {
color: black;
}
All the paragraphs are black. That makes unlayered CSS a convenient “override everything” place, and also a trap: if you layer some of your CSS and leave the rest unlayered, the unlayered part dominates. The healthiest approach is to put all your own CSS in layers.
Layers and !important (it reverses!)
!important flips the order inside layers: an important declaration in an earlier layer beats an important declaration in a later one:
normal declarations: reset < base < components < utilities < unlayered
!important declarations: unlayered < utilities < components < base < reset
This is deliberate: a reset layer can protect something with !important that nothing later can undo. You will rarely need it, but it explains an odd result if it ever happens. (Do not scatter !important. The point of layers is that you rarely need it.)
Declare the order first
The order of layers is fixed at the first time each name appears. That is why the one-line declaration goes at the very top, before any content:
@layer reset, base, components, utilities;
After that you can add to a layer in many places, from different files, and it still keeps its position.
Layers for third-party CSS
Layers shine when you use a framework or library. Import it into a low-priority layer and your own CSS wins without any specificity tricks:
@layer vendor, base, components, utilities;
@import url("library.css") layer(vendor);
Everything in library.css is now weaker than your layers, so you can override it with a simple class. (@import rules must come first in the file, before other rules, apart from the @layer declaration.)
Nested layers and anonymous layers
Layers can nest, with a dot to refer to them from outside:
@layer components {
@layer buttons { .btn { padding: 8px 14px; } }
@layer cards { .card { padding: 20px; } }
}
/* add to a nested layer later */
@layer components.buttons {
.btn-large { padding: 12px 22px; }
}
A layer without a name (@layer { ... }) is allowed, but you cannot add to it later, so named layers are better.
Putting nesting and layers together
Layers organise your stylesheet at the top level. Nesting organises inside a component. They fit together naturally:
@layer reset, base, components, utilities;
@layer reset {
*, *::before, *::after {
box-sizing: border-box;
}
}
@layer base {
body {
font-family: Arial, Helvetica, sans-serif;
line-height: 1.6;
}
}
@layer components {
.card {
max-width: 420px;
padding: 20px;
margin-bottom: 32px;
border: 1px solid #e2e8f0;
border-radius: 12px;
transition: box-shadow 0.2s ease;
&:hover {
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.12);
}
.card-title { margin: 0 0 8px; color: #0f172a; }
.card-text { margin: 0 0 12px; color: #475569; }
.btn {
padding: 8px 14px;
border: none;
border-radius: 6px;
background-color: royalblue;
color: white;
font: inherit;
cursor: pointer;
&:hover { background-color: #1d4ed8; }
&.btn-danger {
background-color: crimson;
&:hover { background-color: #9f1239; }
}
}
}
.layer-demo .message {
padding: 12px 16px;
border-radius: 8px;
background-color: #eff6ff;
color: #1e40af;
}
}
@layer utilities {
.urgent {
background-color: #fef2f2;
color: #b91c1c;
font-weight: bold;
}
}
This is a professional-grade structure: predictable priority between groups, tidy rules inside them, and no !important.
Common mistakes
- Forgetting
&when attaching:hoveror a class to the parent.:hoveralone inside a rule means “a hovered descendant”. - Expecting
&-suffixto work like in Sass. - Nesting too deep. More than three levels gives selectors that are hard to read and to override.
- Leaving some styles unlayered while the rest is in layers, so the unlayered ones always win.
- Declaring layers in the wrong order, or declaring the order after using the layers. The first appearance sets the position.
- Expecting layer order to follow the order of the blocks in the file. Only the
@layer a, b, c;declaration (or first mention) counts. - Forgetting that
!importantreverses the layer order. - Putting
@import ... layer()after other rules. Imports must come first. - Using layers to hide a messy structure. They help organisation, but they do not replace good naming.
Practice
- Click Practice in Editor. Rewrite your card CSS using nesting, so every rule that mentions
.cardsits inside a single.card { }block. - Add a
&:hovershadow to the card. Then remove the&and see why it stops working. - Add a link inside the card and give it a nested
&:hoverunderline. - Style the two buttons with a nested
.btnrule, and the danger variant with&.btn-danger. - Move a media query inside
.cardthat increases the padding atmin-width: 700px. - Declare
@layer reset, base, components, utilities;and put your CSS into the four layers. - Make the urgent message win with a weak
.urgentselector (0-1-0) against a strong.layer-demo .message(0-2-0), by relying on layer order. - Swap the order in the
@layerdeclaration. Which message changes? - Move one rule outside any layer and see that it now beats the layered ones.
Recap
- Native nesting puts related rules inside each other, with no preprocessor. Descendants can be written directly, and
&stands for the parent. - Use
&:hover,&.classand.context &when you need to attach to the parent itself.&-suffixdoes not work. - You can nest
@mediaand@containerinside a rule. Stay within three levels. - Cascade layers (
@layer) rank groups of styles. Declare the order once:@layer reset, base, components, utilities;. A later layer beats an earlier one regardless of specificity. - Unlayered styles beat all layered styles, and
!importantreverses the layer order. - Import third-party CSS into a low layer with
@import url(...) layer(vendor)so your styles win easily. - Layers organise the whole stylesheet and nesting organises each component. Use them together.
- Next: architecture patterns such as BEM, plus performance and accessibility.