CSS Nesting: Complete Guide with Examples
Native CSS nesting is here. For years, nesting was the single most compelling reason developers reached for Sass or Less. Now the browser handles it natively, with zero build tools, zero dependencies, and zero compilation step. This guide covers everything you need to know: syntax, the & selector, nested at-rules, specificity implications, migration from preprocessors, and production best practices.
1. What is CSS Nesting?
CSS nesting allows you to write child, descendant, and contextual style rules inside a parent rule block. Instead of repeating the parent selector across multiple flat rules, you nest related styles together. The feature is defined in the CSS Nesting Module Level 1 specification.
Before nesting, you wrote flat CSS like this:
/* Flat CSS — repetitive selectors */
.card { background: #fff; border-radius: 8px; }
.card .title { font-size: 1.25rem; font-weight: 700; }
.card .title span { color: #6b7280; }
.card .body { padding: 1rem; }
.card .body p { line-height: 1.6; }
.card:hover { box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
With native CSS nesting, the same styles become:
/* Nested CSS — grouped by component */
.card {
background: #fff;
border-radius: 8px;
.title {
font-size: 1.25rem;
font-weight: 700;
span { color: #6b7280; }
}
.body {
padding: 1rem;
p { line-height: 1.6; }
}
&:hover {
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
}
The browser parses the nested version and applies styles identically to the flat version. No build step, no preprocessor, no extra tooling. This is native to the CSS language.
2. Browser Support
CSS nesting has excellent support across modern evergreen browsers as of early 2026:
| Browser | Version | Release Date | Notes |
|---|---|---|---|
| Chrome | 120+ | Dec 2023 | Relaxed parsing (bare type selectors allowed) |
| Edge | 120+ | Dec 2023 | Same engine as Chrome |
| Firefox | 117+ | Aug 2023 | Full support including relaxed parsing |
| Safari | 17.2+ | Dec 2023 | Relaxed parsing in 17.2 |
| Opera | 106+ | Dec 2023 | Chromium-based |
Global coverage: Over 90% of users worldwide. The main holdouts are older Safari on iOS 16 and earlier, and IE11 (which is end-of-life).
You can detect support at runtime with @supports:
/* Feature detection for CSS nesting */
@supports selector(&) {
/* Nesting is supported — use nested rules here */
.card {
.title { font-weight: 700; }
}
}
/* Fallback for older browsers */
@supports not selector(&) {
.card .title { font-weight: 700; }
}
For build-time fallback, the postcss-nesting plugin compiles nested CSS to flat selectors that work everywhere.
3. Basic Nesting Syntax
The fundamental rule: any style rule can contain other style rules inside its block. The nested rule's selector is combined with the parent's selector to form the full selector.
Simple class and ID selectors
.nav {
display: flex;
gap: 1rem;
.link {
color: #3b82f6;
text-decoration: none;
}
.link.active {
font-weight: 700;
border-bottom: 2px solid currentColor;
}
#logo {
font-size: 1.5rem;
}
}
The browser desugars this to:
.nav { display: flex; gap: 1rem; }
.nav .link { color: #3b82f6; text-decoration: none; }
.nav .link.active { font-weight: 700; border-bottom: 2px solid currentColor; }
.nav #logo { font-size: 1.5rem; }
Type (element) selectors
Since Chrome 120 and Safari 17.2 (the "relaxed parsing" update), you can nest bare type selectors without the & prefix:
.article {
line-height: 1.7;
h2 {
font-size: 1.5rem;
margin-top: 2rem;
}
p {
margin-bottom: 1rem;
}
ul {
padding-left: 1.5rem;
list-style: disc;
}
}
Earlier browser versions (Chrome 112–119, Safari 16.5–17.1) required & h2 instead of bare h2. The relaxed syntax is now the standard.
Multiple nested rules
You can nest as many rules as you need. Declarations (properties) and nested rules can be freely interleaved:
.btn {
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
.icon {
margin-right: 0.5rem;
width: 1em;
height: 1em;
}
.label {
font-weight: 600;
}
}
4. The & Nesting Selector
The & selector explicitly represents the parent selector. It is required in some contexts and optional in others. Understanding when and how to use it is the key to mastering CSS nesting.
Appending to the parent (pseudo-classes and pseudo-elements)
When you need to append to the parent selector with no space, use & directly:
.btn {
background: #3b82f6;
color: #fff;
&:hover {
background: #2563eb;
}
&:focus-visible {
outline: 2px solid #60a5fa;
outline-offset: 2px;
}
&:active {
transform: scale(0.98);
}
&::before {
content: "";
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 0.5rem;
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
This desugars to .btn:hover, .btn:focus-visible, .btn::before, etc. Without the &, a space would be inserted and the browser would look for a descendant element matching :hover, which is not what you want.
Compound selectors (appending a class)
.btn {
background: #e5e7eb;
&.primary {
background: #3b82f6;
color: #fff;
}
&.danger {
background: #ef4444;
color: #fff;
}
&.outline {
background: transparent;
border: 2px solid currentColor;
}
}
Desugars to .btn.primary, .btn.danger, .btn.outline. The & concatenates with no space.
Prepending — reversing the nesting direction
Place & after another selector to prepend a parent context:
.title {
font-size: 1rem;
/* When .title is inside .hero */
.hero & {
font-size: 2.5rem;
font-weight: 800;
}
/* When html has dark mode class */
.dark & {
color: #e4e4e7;
}
/* Right-to-left context */
[dir="rtl"] & {
text-align: right;
}
}
This desugars to .hero .title, .dark .title, and [dir="rtl"] .title. The & is replaced with the parent selector wherever it appears.
Multiple uses of & in one rule
.item {
/* .item + .item (adjacent sibling) */
& + & {
margin-top: 1rem;
}
/* .item.item (double specificity boost) */
&& {
font-weight: 700;
}
}
5. Nesting Media Queries and @rules
One of the most practical benefits of CSS nesting is placing @media, @supports, @layer, and other conditional at-rules directly inside a style rule. This keeps responsive logic co-located with the component it affects.
Nested @media
.sidebar {
display: none;
@media (min-width: 768px) {
display: block;
width: 250px;
}
@media (min-width: 1024px) {
width: 300px;
}
@media (prefers-reduced-motion: reduce) {
transition: none;
}
}
This desugars to:
.sidebar { display: none; }
@media (min-width: 768px) { .sidebar { display: block; width: 250px; } }
@media (min-width: 1024px) { .sidebar { width: 300px; } }
@media (prefers-reduced-motion: reduce) { .sidebar { transition: none; } }
No more hunting through your stylesheet for scattered media query blocks that reference the same selector.
Nested @supports
.grid-layout {
display: flex;
flex-wrap: wrap;
@supports (display: grid) {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 1rem;
}
@supports (container-type: inline-size) {
container-type: inline-size;
}
}
Nested @layer
.component {
@layer base {
display: block;
padding: 1rem;
}
@layer theme {
background: var(--surface);
color: var(--text);
}
}
Combining nested at-rules with nested selectors
.card {
padding: 1rem;
display: flex;
flex-direction: column;
.title {
font-size: 1rem;
@media (min-width: 768px) {
font-size: 1.25rem;
}
}
.image {
width: 100%;
aspect-ratio: 16 / 9;
@media (min-width: 1024px) {
aspect-ratio: 21 / 9;
}
}
@media (min-width: 768px) {
flex-direction: row;
padding: 2rem;
}
}
At-rules and selector rules can be nested at any level, and they can be nested inside each other. The browser resolves each combination correctly.
6. Nesting Combinators
CSS combinators (>, +, ~, and the descendant space) work naturally inside nested rules.
Child combinator (>)
.nav {
> li {
display: inline-block;
> a {
padding: 0.5rem 1rem;
text-decoration: none;
}
}
}
Desugars to .nav > li and .nav > li > a.
Adjacent sibling (+)
.heading {
margin-bottom: 0.5rem;
+ p {
margin-top: 0;
font-size: 1.1rem;
}
+ .subheading {
color: #6b7280;
}
}
Desugars to .heading + p and .heading + .subheading.
General sibling (~)
.toggle:checked {
~ .panel {
display: block;
opacity: 1;
}
~ .label {
font-weight: 700;
}
}
Combining combinators with &
.list-item {
padding: 0.75rem;
border-bottom: 1px solid #e5e7eb;
/* .list-item + .list-item */
& + & {
border-top: none;
}
/* .list > .list-item */
.list > & {
padding-left: 1.5rem;
}
}
7. Specificity and Nesting
This is the most important section for avoiding bugs. Native CSS nesting and Sass nesting produce different specificity due to how the browser desugars nested selectors.
The :is() wrapping rule
When the browser desugars a nested rule, the parent selector is wrapped in :is() if it is a selector list. The :is() pseudo-class takes the specificity of its most specific argument.
/* What you write */
.card, #featured {
.title {
color: red;
}
}
/* What the browser computes */
:is(.card, #featured) .title {
color: red;
}
/* Specificity: (1,0,1) — the #featured ID specificity
applies even when matching via .card */
In Sass, the same nesting would produce two separate rules:
/* Sass output */
.card .title { color: red; } /* Specificity: (0,2,0) */
#featured .title { color: red; } /* Specificity: (1,0,1) */
With native CSS nesting, .card .title has specificity (1,0,1) because :is(.card, #featured) takes the higher specificity of #featured. This can cause unexpected overrides.
Simple parent selectors (no list)
When the parent is a single selector (not a comma-separated list), no :is() wrapping occurs and specificity works exactly as you would expect:
/* Single parent — no :is() wrapping */
.card {
.title { color: red; }
}
/* Desugars to: .card .title { color: red; }
Specificity: (0,2,0) — as expected */
Practical specificity tips
- Avoid mixing IDs and classes in selector lists that are used as nesting parents. Split them into separate rules instead.
- Use
:where()to strip specificity when you need zero-specificity defaults::where(.card) { .title { } }. - Check DevTools. Chrome and Firefox DevTools show the desugared selector, making it easy to inspect the actual specificity.
8. Complex Nesting Patterns
Pseudo-classes
.input {
border: 1px solid #d1d5db;
&:focus {
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59,130,246,0.3);
}
&:invalid {
border-color: #ef4444;
}
&:placeholder-shown {
color: #9ca3af;
}
&:not(:placeholder-shown):valid {
border-color: #22c55e;
}
}
Pseudo-elements
.tooltip {
position: relative;
&::before {
content: attr(data-tip);
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
padding: 0.25rem 0.5rem;
background: #1f2937;
color: #fff;
font-size: 0.75rem;
border-radius: 4px;
white-space: nowrap;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s;
}
&:hover::before {
opacity: 1;
}
}
Attribute selectors
.form-field {
margin-bottom: 1rem;
[type="text"], [type="email"], [type="password"] {
width: 100%;
padding: 0.5rem;
border: 1px solid #d1d5db;
border-radius: 4px;
}
[required]::after {
content: " *";
color: #ef4444;
}
&[data-state="error"] {
.label { color: #ef4444; }
.message { display: block; }
}
}
Nesting inside :is(), :has(), and :where()
.card {
background: #fff;
&:has(.image) {
/* Card contains an image */
grid-template-rows: auto 1fr;
.title {
font-size: 1.25rem;
}
}
&:has(> .badge) {
padding-top: 2rem;
}
}
Real-world component example
.dropdown {
position: relative;
display: inline-block;
.trigger {
padding: 0.5rem 1rem;
border: 1px solid #d1d5db;
border-radius: 4px;
cursor: pointer;
&:hover { background: #f9fafb; }
&::after {
content: "\25BC";
margin-left: 0.5rem;
font-size: 0.7em;
}
}
.menu {
position: absolute;
top: 100%;
left: 0;
min-width: 200px;
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 4px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
display: none;
.item {
padding: 0.5rem 1rem;
&:hover { background: #f3f4f6; }
& + .item { border-top: 1px solid #f3f4f6; }
}
}
&.open .menu {
display: block;
}
}
9. Migrating from Sass/Less to Native CSS Nesting
If you are coming from Sass or Less, most nested code transfers directly. However, there are critical differences to know.
Differences at a glance
| Feature | Sass/Less | Native CSS |
|---|---|---|
| Bare type selectors | div { } works | Works in Chrome 120+, Safari 17.2+, Firefox 117+ |
| String concatenation | &__element creates .block__element | Not supported. &__element is invalid |
| Specificity | Flat output, normal specificity | :is() wrapping for selector lists |
| Variables | $var / @var | var(--custom-prop) |
| Mixins | @include / .mixin() | No equivalent (use custom properties or @apply proposal) |
| Nesting limit | No limit | No limit (but 3 levels recommended) |
The BEM concatenation problem
This is the biggest migration gotcha. Sass allows string concatenation with &:
/* Sass — works fine */
.block {
&__element { padding: 1rem; }
&--modifier { color: red; }
}
/* Output: .block__element, .block--modifier */
Native CSS does not support this. The & represents the full parent selector as a selector object, not a string. You must use flat selectors or restructure:
/* Native CSS — correct approach */
.block {
.block__element { padding: 1rem; }
.block--modifier { color: red; }
}
/* Or use data attributes instead of BEM */
.block {
[data-element="title"] { padding: 1rem; }
&[data-variant="alt"] { color: red; }
}
Migration strategy
- Start with new components. Write new CSS using native nesting. Do not rewrite your entire codebase at once.
- Remove Sass nesting that uses string concatenation. Replace
&__elementwith explicit class selectors. - Replace Sass variables with CSS custom properties where possible. See our CSS Variables Complete Guide.
- Test specificity. Run your existing test suite or visual regression tests, since :is() wrapping can change cascade behavior.
- Use PostCSS as a bridge. The
postcss-nestingplugin compiles native nesting syntax to flat CSS, giving you the new syntax with old-browser support.
Side-by-side comparison
/* Sass */
.nav {
display: flex;
&__list {
list-style: none;
&--horizontal { flex-direction: row; }
}
&__item {
&:hover { background: #f0f0f0; }
&.active { font-weight: bold; }
}
}
/* Native CSS equivalent */
.nav {
display: flex;
.nav__list {
list-style: none;
&.nav__list--horizontal { flex-direction: row; }
}
.nav__item {
&:hover { background: #f0f0f0; }
&.active { font-weight: bold; }
}
}
10. Performance Considerations
Native CSS nesting has no meaningful performance penalty compared to flat CSS. The browser's style engine has been parsing and matching selectors for decades, and the desugaring step is trivial.
- Parse time: Nested CSS is slightly faster to parse than equivalent flat CSS because the parser processes the parent selector once and reuses context for children. The difference is negligible for any real stylesheet.
- Selector matching: The desugared selectors are matched using the same engine as flat selectors. There is no runtime overhead from nesting itself.
- :is() overhead: The :is() wrapping used for selector lists adds no measurable cost. Browsers optimize :is() matching at the engine level.
- File size: Nested CSS is typically 10–20% smaller than equivalent flat CSS because parent selectors are not repeated. Less bytes transferred means faster load times.
- DevTools: Chrome, Firefox, and Safari DevTools all display nested CSS correctly and show the computed flat selectors in the Styles panel.
Use our CSS Minifier to compress your nested stylesheets for production, and our CSS Beautifier to format them for development.
11. Best Practices
Limit nesting depth to 3 levels
/* Good: 2 levels */
.card {
.header {
.title { font-size: 1.25rem; }
}
}
/* Bad: 5 levels — hard to read, high specificity */
.page {
.main {
.section {
.card {
.title { font-size: 1.25rem; }
}
}
}
}
Keep declarations before nested rules
/* Good: declarations first, nested rules after */
.card {
padding: 1rem;
background: #fff;
border-radius: 8px;
.title { font-weight: 700; }
.body { line-height: 1.6; }
}
/* Avoid: interleaving declarations and nested rules */
.card {
padding: 1rem;
.title { font-weight: 700; }
background: #fff;
.body { line-height: 1.6; }
border-radius: 8px;
}
Use & explicitly for clarity
Even though bare type selectors are allowed, consider using & when the intent might be ambiguous:
.widget {
/* Clear: this is .widget:hover */
&:hover { background: #f9fafb; }
/* Clear: this is .widget.active */
&.active { border-color: #3b82f6; }
/* Bare nesting is fine for descendants */
.title { font-size: 1.1rem; }
}
Group related rules
.btn {
/* Base styles */
display: inline-flex;
align-items: center;
padding: 0.5rem 1rem;
border-radius: 4px;
/* Variants */
&.primary { background: #3b82f6; color: #fff; }
&.secondary { background: #6b7280; color: #fff; }
&.ghost { background: transparent; color: #3b82f6; }
/* States */
&:hover { filter: brightness(1.1); }
&:active { transform: scale(0.98); }
&:disabled { opacity: 0.5; pointer-events: none; }
/* Children */
.icon { margin-right: 0.5rem; }
.label { font-weight: 600; }
/* Responsive */
@media (max-width: 640px) {
width: 100%;
justify-content: center;
}
}
Debug with DevTools
All major browsers show nested rules in the Styles panel. Chrome DevTools displays the original nested source alongside the computed flat selector, making it easy to trace specificity issues. Use the "Computed" tab to see which rules win the cascade.
Combine with CSS custom properties
.card {
--card-padding: 1rem;
--card-radius: 8px;
padding: var(--card-padding);
border-radius: var(--card-radius);
.header {
padding: calc(var(--card-padding) * 0.75);
border-bottom: 1px solid #e5e7eb;
}
@media (min-width: 768px) {
--card-padding: 1.5rem;
--card-radius: 12px;
}
}
Native CSS nesting combined with CSS custom properties gives you most of what Sass provided, with no build step. Pair these with CSS Grid and Flexbox for a complete, modern CSS architecture.
Summary
Native CSS nesting removes the last major reason most teams reached for a CSS preprocessor. Here are the key points:
- No build step required. Native nesting works in every modern browser (Chrome 120+, Firefox 117+, Safari 17.2+).
- Use
&for pseudo-classes, pseudo-elements, and compound selectors. Bare class and type selectors nest without it. - Nest @media and @supports inside rules to keep responsive and progressive-enhancement styles co-located with components.
- Watch specificity with selector lists. The browser wraps parent selector lists in
:is(), which takes the highest specificity argument. - No BEM string concatenation. Replace
&__elementpatterns with explicit class selectors. - Limit depth to 3 levels. Keep stylesheets readable and specificity manageable.
- Combine with CSS custom properties for a powerful, zero-dependency styling system.
Frequently Asked Questions
What is native CSS nesting and how is it different from Sass nesting?
Native CSS nesting is a built-in browser feature that lets you write nested style rules directly in your stylesheets without any preprocessor. Unlike Sass nesting, which is compiled to flat CSS at build time, native CSS nesting is parsed and applied by the browser at runtime. The syntax is very similar, but there are key differences: native CSS nesting requires the & selector when nesting type selectors in older browsers, and specificity is calculated differently because the browser wraps implicit selectors with :is(). Native CSS nesting works without any build tools, compilers, or dependencies.
Can I nest media queries inside a CSS rule?
Yes, one of the most powerful features of CSS nesting is the ability to nest @media, @supports, @layer, and other at-rules directly inside a style rule. This keeps related responsive styles co-located with the component they affect. For example, you can write .card { padding: 1rem; @media (min-width: 768px) { padding: 2rem; } } and the browser applies the nested media query to the .card selector. This eliminates the need to repeat selectors across separate media query blocks.
Do all browsers support CSS nesting?
As of early 2026, CSS nesting is supported in all major evergreen browsers: Chrome 120+, Firefox 117+, Safari 17.2+, and Edge 120+. This covers over 90% of global browser usage. The relaxed parsing rules (allowing bare type selectors without &) shipped in Chrome 120 and Safari 17.2. For older browsers, you can use a PostCSS plugin like postcss-nesting as a build-time fallback that compiles nested CSS to flat selectors.
How does specificity work with CSS nesting?
When you nest selectors in CSS, the browser desugars them using :is() wrapping for selector lists. For example, .card, #featured { .title { } } becomes :is(.card, #featured) .title { }. The :is() pseudo-class takes the specificity of its most specific argument. This means nested selectors may have higher specificity than you expect if the parent is a selector list containing a high-specificity selector like an ID. When the parent is a single selector, no :is() wrapping occurs and specificity is calculated normally.
How deep should I nest CSS rules?
The widely accepted best practice is to limit nesting to 3 levels deep, matching the guideline used in Sass communities for years. Beyond 3 levels, selectors become hard to read, specificity escalates unpredictably, and styles become tightly coupled to HTML structure. Deep nesting also makes debugging harder because browser DevTools show the desugared flat selectors, not the nested source. If you find yourself nesting deeper than 3 levels, refactor using BEM-style flat classes, CSS custom properties for shared values, or break the component into smaller pieces.