Skip to main content

CSS Nesting: Native Sass-Like Nesting in Plain CSS

CSS nesting is now a native browser feature — no Sass, no build step required. Write nested rules directly in plain CSS and the browser handles them. This page covers the syntax, specificity behavior, key differences from Sass, and how to guide AI to use nesting correctly.


1. What CSS Nesting Is

/* WITHOUT nesting (traditional flat CSS) */
.c-card { background: var(--color-surface); }
.c-card__image { width: 100%; aspect-ratio: 16/9; }
.c-card__title { font-size: var(--text-lg); }
.c-card:hover { box-shadow: var(--shadow-md); }

/* WITH nesting */
.c-card {
background: var(--color-surface);

.c-card__image { /* Nested selector */
width: 100%;
aspect-ratio: 16/9;
}

.c-card__title {
font-size: var(--text-lg);
}

&:hover { /* & = the parent selector (.c-card) */
box-shadow: var(--shadow-md);
}
}

Both versions produce identical CSS output. Nesting is a syntax convenience, not a different cascade model.


2. The Nesting Selector &

The & is the nesting selector — it explicitly refers to the parent selector:

.c-btn {
background: var(--color-primary);
color: white;

/* &:hover = .c-btn:hover */
&:hover {
background: var(--color-primary-dark);
transform: translateY(-2px);
}

/* &:focus-visible = .c-btn:focus-visible */
&:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 3px;
}

/* &:active = .c-btn:active */
&:active {
transform: translateY(0);
}

/* &.is-loading = .c-btn.is-loading */
&.is-loading {
opacity: 0.7;
pointer-events: none;
cursor: wait;
}

/* &--modifier = .c-btn--modifier (BEM modifier) */
&--outline {
background: transparent;
border: 2px solid var(--color-primary);
color: var(--color-primary);
}

&--ghost {
background: transparent;
color: var(--color-primary);
&:hover { background: rgba(var(--color-primary-rgb), 0.1); }
}

&--sm { padding: 0.4rem 0.9rem; font-size: var(--text-sm); }
&--lg { padding: 1rem 2rem; font-size: var(--text-lg); }
}

3. Nesting Without & (Implicit Descendant)

When you nest without &, the nested selector behaves as a descendant (equivalent to & .nested):

.c-nav {
display: flex;

/* Without &: .c-nav .c-nav__list (descendant) */
.c-nav__list {
display: flex;
gap: 0.5rem;
list-style: none;
}

/* Without &: .c-nav .c-nav__link (descendant) */
.c-nav__link {
color: var(--color-text-muted);
text-decoration: none;
padding: 0.5rem 1rem;

/* Nested inside .c-nav__link: .c-nav .c-nav__link:hover */
&:hover { color: var(--color-primary); }

/* State class: .c-nav .c-nav__link.is-active */
&.is-active { color: var(--color-primary); font-weight: 600; }
}
}

[!IMPORTANT] Implicit descendant nesting (no &) does NOT work for pseudo-elements and pseudo-classes at the top level of the nest. You MUST use & for those:

.c-card {
:hover { } /* ❌ Invalid — no & means no valid selector */
&:hover { } /* ✅ Correct */
}

4. Nesting @media and Other At-Rules Inside Selectors

One of the most powerful features — media queries nested inside components:

/* Component-level responsive design — much more readable */
.c-hero {
min-height: 60dvh;
display: grid;
grid-template-columns: 1fr; /* Mobile: single column */

/* Nest the media query inside the component rule */
@media (min-width: 768px) {
grid-template-columns: 1fr 1fr; /* Tablet: two columns */
min-height: 80dvh;
}

@media (min-width: 1024px) {
grid-template-columns: 1fr 2fr; /* Desktop: 1/3 and 2/3 */
min-height: 100dvh;
}

.c-hero__content {
padding: 2rem;

@media (min-width: 768px) {
padding: 4rem;
}
}
}
/* Nesting @layer inside selectors */
@layer components {
.c-card {
background: var(--color-surface);

@layer overrides {
/* Lower specificity within the same block */
background: var(--color-surface-2);
}
}
}

/* Nesting @supports for progressive enhancement */
.c-glass {
background: var(--color-surface);

@supports (backdrop-filter: blur(1px)) {
background: rgba(255, 255, 255, 0.1);
-webkit-backdrop-filter: blur(20px);
backdrop-filter: blur(20px);
}
}

5. Nesting Specificity Rules

Nesting does not change specificity — the output is the same as writing the selectors flat:

/* Nested version */
.c-card {
.c-card__title { color: red; }
}

/* Flat equivalent */
.c-card .c-card__title { color: red; }
/* Specificity: 0,2,0 (two class selectors) */

/* Using & */
.c-card {
&:hover { background: blue; }
}
/* Equivalent: .c-card:hover — Specificity: 0,1,1 */
/* Watch out: nesting stacking can increase specificity */
.c-nav {
.c-nav__list {
.c-nav__item {
.c-nav__link { color: red; }
/* Equivalent: .c-nav .c-nav__list .c-nav__item .c-nav__link */
/* Specificity: 0,4,0 — four class selectors! */
/* This is exactly why flat BEM avoids deep nesting */
}
}
}

/* ✅ Better: keep nesting to 2 levels maximum */
.c-nav {
.c-nav__link { color: var(--color-text-muted); }
.c-nav__link:hover { color: var(--color-primary); }
}

6. Native CSS Nesting vs Sass Nesting

FeatureNative CSSSass/SCSS
Works without build step✅ Yes❌ No
& nesting selector✅ Yes✅ Yes
&--modifier BEM shorthand✅ Yes✅ Yes
Nested @media✅ Yes✅ Yes
Nested @layer✅ Yes⚠ Limited
Variables ($var)❌ No — use --custom-props✅ Yes
Mixins❌ No✅ Yes
FunctionsLimited (custom functions)✅ Yes
Loops (@each, @for)❌ No✅ Yes
Browser support96%+Compiled to CSS
/* Sass: interpolation in & is more powerful */
.c-btn {
@each $variant in primary, secondary, danger {
&--#{$variant} { background: var(--color-#{$variant}); }
}
}
/* Outputs: .c-btn--primary, .c-btn--secondary, .c-btn--danger */

/* Native CSS: no loops — write each variant manually */
.c-btn {
&--primary { background: var(--color-primary); }
&--secondary { background: var(--color-secondary); }
&--danger { background: var(--color-danger); }
}

7. When to Use — and Not Use — Nesting

✅ Good Uses of Nesting

/* 1. Pseudo-classes and pseudo-elements on a component */
.c-card {
&:hover { box-shadow: var(--shadow-md); }
&:focus-visible { outline: 2px solid var(--color-primary); }
&::before { content: ''; display: block; }
}

/* 2. State classes */
.c-accordion {
&.is-open .c-accordion__content { display: block; }
&.is-loading { opacity: 0.6; pointer-events: none; }
}

/* 3. Nested @media for component-level responsiveness */
.c-hero {
padding: 3rem;
@media (min-width: 1024px) { padding: 6rem; }
}

/* 4. BEM element variations within the modifier */
.c-card--featured {
.c-card__title { font-size: var(--text-2xl); }
.c-card__image { aspect-ratio: 21/9; }
}

❌ Avoid Deep Nesting

/* ❌ Never do this — deep nesting = high specificity + hard to read */
.c-nav {
.c-nav__list {
.c-nav__item {
.c-nav__link {
&:hover {
.c-nav__link-icon { color: red; }
}
}
}
}
}
/* Specificity: 0,5,1 — almost impossible to override */

/* ✅ Maximum 2 levels of nesting in production */
.c-nav {
.c-nav__link { color: var(--color-text-muted); }
.c-nav__link:hover { color: var(--color-primary); }
}

8. Browser Support

BrowserSupportVersion
Chrome112+
Firefox117+
Safari17.2+
Edge112+
Global~95%

[!TIP] CSS Nesting is safe to use in production today. For the ~5% of legacy browsers, nesting degrades gracefully — browsers that don't understand it simply ignore the nested rules.


9. AI and CSS Nesting

AI tools (Claude, ChatGPT, Copilot) generate nested CSS by default since their training data includes both Sass and native CSS nesting. Know how to guide this:

Prompt addition for native CSS nesting:
"Use native CSS nesting (no Sass required).
Maximum 2 levels of nesting depth.
Use & for pseudo-classes, pseudo-elements, and state classes.
Use nested @media for component-level responsive behavior."

Prompt addition to DISABLE nesting:
"Do NOT use CSS nesting syntax.
Write all selectors on the top level (flat BEM structure).
This is for a WordPress site without a build step that may
need to support Safari 16 (no nesting support)."
/* What AI typically produces (good): */
.c-card {
background: var(--color-surface);
border-radius: var(--radius-lg);

&:hover {
box-shadow: var(--shadow-md);
transform: translateY(-2px);
}

.c-card__title {
font-size: var(--text-xl);
font-weight: 700;
}
}

/* Check for: max depth 2, no interpolation, no Sass-only features */